L'utilisation de malloc()
et free()
semble assez rare dans le monde Arduino. Il est utilisé beaucoup plus souvent dans l'AVR C pur, mais avec prudence.
Est-ce une très mauvaise idée d'utiliser malloc()
et free()
avec Arduino?
L'utilisation de malloc()
et free()
semble assez rare dans le monde Arduino. Il est utilisé beaucoup plus souvent dans l'AVR C pur, mais avec prudence.
Est-ce une très mauvaise idée d'utiliser malloc()
et free()
avec Arduino?
Réponses:
Ma règle générale pour les systèmes embarqués est de n'utiliser que de malloc()
grands tampons et une seule fois, au début du programme, par exemple, dans setup()
. Le problème survient lorsque vous allouez et dé-allouez de la mémoire. Au cours d'une session de longue durée, la mémoire devient fragmentée et finalement, une allocation échoue en raison de l'absence d'une zone libre suffisamment grande, même si la mémoire libre totale est plus que suffisante pour la demande.
(Perspective historique, ignorer si non intéressé): Selon l’implémentation du chargeur, le seul avantage de l’allocation d’exécution par rapport à l’allocation de compilation (globales initialisées) est la taille du fichier hexadécimal. Lorsque les systèmes intégrés étaient construits avec des ordinateurs standard disposant de toute la mémoire volatile, le programme était souvent chargé sur le système intégré à partir d'un réseau ou d'un ordinateur d'instrumentation et le temps de téléchargement posait parfois problème. Laisser des zones pleines de zéros dans l’image pourrait raccourcir considérablement le temps.)
Si j’ai besoin d’une allocation de mémoire dynamique dans un système intégré, j’alloue généralement malloc()
, ou de préférence, de manière statique un grand pool et le divise en tampons de taille fixe (ou un pool, chacun de petits et de grands tampons, respectivement) et effectue ma propre allocation /. désaffectation de ce pool. Ensuite, chaque demande pour une quantité de mémoire allant jusqu'à la taille de tampon fixe est honorée avec l'un de ces tampons. La fonction appelante n'a pas besoin de savoir si elle est plus grande que celle demandée, et en évitant de fractionner et de regrouper à nouveau des blocs, nous résolvons la fragmentation. Bien sûr, des fuites de mémoire peuvent toujours se produire si le programme dispose de bogues d’allocation / dé-allocation.
En règle générale, lors de l'écriture d'esquisses Arduino, vous éviterez l'allocation dynamique (que ce soit avec malloc
ou new
pour les instances C ++), mais les utilisateurs utilisent plutôt des static
variables globales ou des variables locales (de pile).
L'utilisation de l'allocation dynamique peut entraîner plusieurs problèmes:
malloc
/ free
appels) où le tas devient plus grand que la quantité réelle de mémoire allouée actuellementDans la plupart des situations que j'ai rencontrées, l'allocation dynamique n'était pas nécessaire ou pouvait être évitée avec des macros, comme dans l'exemple de code suivant:
MySketch.ino
#define BUFFER_SIZE 32
#include "Dummy.h"
Dummy.h
class Dummy
{
byte buffer[BUFFER_SIZE];
...
};
Sans cela #define BUFFER_SIZE
, si nous voulions que la Dummy
classe ait une buffer
taille non fixe , nous devrions utiliser l'allocation dynamique comme suit:
class Dummy
{
const byte* buffer;
public:
Dummy(int size):buffer(new byte[size])
{
}
~Dummy()
{
delete [] bufer;
}
};
Dans ce cas, nous avons plus d'options que dans le premier exemple (par exemple, utiliser différents Dummy
objets avec une buffer
taille différente pour chacun), mais nous pouvons avoir des problèmes de fragmentation de tas.
Notez l'utilisation d'un destructeur pour s'assurer que la mémoire allouée dynamiquement buffer
sera libérée lorsqu'une Dummy
instance est supprimée.
J’ai jeté un coup d’œil à l’algorithme utilisé par malloc()
avr-libc et il semble y avoir quelques modèles d’utilisation qui sont sécuritaires du point de vue de la fragmentation de tas:
J'entends par là: allouer tout ce dont vous avez besoin au début du programme et ne jamais le libérer. Bien sûr, dans ce cas, vous pouvez également utiliser des tampons statiques ...
Signification: vous libérez le tampon avant d’allouer quoi que ce soit. Un exemple raisonnable pourrait ressembler à ceci:
void foo()
{
size_t size = figure_out_needs();
char * buffer = malloc(size);
if (!buffer) fail();
do_whatever_with(buffer);
free(buffer);
}
S'il n'y a pas de malloc à l'intérieur do_whatever_with()
, ou si cette fonction libère tout ce qu'elle alloue, vous êtes à l'abri de la fragmentation.
Ceci est une généralisation des deux cas précédents. Si vous utilisez le tas comme une pile (le dernier entré est le premier sorti), il se comportera alors comme une pile et non pas comme un fragment. Il convient de noter que dans ce cas, il est prudent de redimensionner le dernier tampon alloué avec realloc()
.
Cela n'empêchera pas la fragmentation, mais cela ne pose aucun problème, car le tas ne deviendra pas plus grand que la taille maximale utilisée . Si tous vos tampons ont la même taille, vous pouvez être sûr que, chaque fois que vous en libérerez un, l'emplacement sera disponible pour des allocations ultérieures.
L'utilisation de l'allocation dynamique (via malloc
/ free
ou new
/ delete
) n'est pas fondamentalement mauvaise en soi. En fait, pour quelque chose comme le traitement de chaîne (par exemple via l' String
objet), c'est souvent très utile. En effet, de nombreux croquis utilisent plusieurs petits fragments de chaînes, qui sont finalement combinés en un plus grand. L'utilisation de l'allocation dynamique ne vous permet d'utiliser que la quantité de mémoire dont vous avez besoin pour chacun d'entre eux. En revanche, l’utilisation d’un tampon statique de taille fixe pour chacun risque de gaspiller beaucoup d’espace (ce qui entraîne un manque de mémoire beaucoup plus rapide), même si cela dépend entièrement du contexte.
Cela étant dit, il est très important de s’assurer que l’utilisation de la mémoire est prévisible. Le fait de permettre à l'esquisse d'utiliser une quantité de mémoire arbitraire en fonction des circonstances d'exécution (par exemple, une entrée) peut facilement poser problème tôt ou tard. Dans certains cas, cela peut être parfaitement sûr, par exemple si vous savez que l’utilisation ne sera jamais très rentable. Les croquis peuvent cependant changer pendant le processus de programmation. Une hypothèse avancée peut être oubliée si quelque chose est modifié ultérieurement, ce qui crée un problème imprévu.
Pour des raisons de robustesse, il est généralement préférable de travailler avec des tampons de taille fixe dans la mesure du possible et de concevoir l'esquisse de manière à ce qu'elle fonctionne explicitement avec ces limites dès le départ. Cela signifie que toute modification future de l'esquisse ou toute situation d'exécution imprévue ne devrait, espérons-le, causer aucun problème de mémoire.
Je ne suis pas d'accord avec les gens qui pensent que vous ne devriez pas l'utiliser ou que c'est généralement inutile. Je crois que cela peut être dangereux si vous n'en connaissez pas les tenants et les aboutissants, mais c'est utile. J'ai des cas où je ne connais pas (et ne devrais pas m'inquiéter) la taille d'une structure ou d'un tampon (au moment de la compilation ou de l'exécution), en particulier en ce qui concerne les bibliothèques que j'envoie au monde. Je conviens que si votre application ne traite que d’une structure unique et connue, vous devez simplement cuire dans cette taille au moment de la compilation.
Exemple: J'ai une classe de paquet série (une bibliothèque) pouvant prendre des données utiles de longueur arbitraire (peut être une structure, un tableau de uint16_t, etc.). Du côté de l'envoi de cette classe, vous indiquez simplement à la méthode Packet.send () l'adresse de l'élément que vous souhaitez envoyer et le port HardwareSerial par lequel vous souhaitez l'envoyer. Cependant, du côté de la réception, il me faut un tampon de réception alloué dynamiquement pour contenir cette charge entrante, car cette charge pourrait être une structure différente à tout moment, en fonction de l'état de l'application, par exemple. SI je n'envoie jamais qu'une seule structure dans les deux sens, je ferais juste que le tampon ait la taille requise au moment de la compilation. Mais, dans le cas où les paquets peuvent avoir des longueurs différentes dans le temps, malloc () et free () ne sont pas si mauvais.
J'ai effectué des tests avec le code suivant pendant des jours, le laissant boucle en boucle, et je n'ai trouvé aucune preuve de fragmentation de la mémoire. Après avoir libéré la mémoire allouée dynamiquement, la quantité disponible revient à sa valeur précédente.
// found at learn.adafruit.com/memories-of-an-arduino/measuring-free-memory
int freeRam () {
extern int __heap_start, *__brkval;
int v;
return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
}
uint8_t *_tester;
while(1) {
uint8_t len = random(1, 1000);
Serial.println("-------------------------------------");
Serial.println("len is " + String(len, DEC));
Serial.println("RAM: " + String(freeRam(), DEC));
Serial.println("_tester = " + String((uint16_t)_tester, DEC));
Serial.println("alloating _tester memory");
_tester = (uint8_t *)malloc(len);
Serial.println("RAM: " + String(freeRam(), DEC));
Serial.println("_tester = " + String((uint16_t)_tester, DEC));
Serial.println("Filling _tester");
for (uint8_t i = 0; i < len; i++) {
_tester[i] = 255;
}
Serial.println("RAM: " + String(freeRam(), DEC));
Serial.println("freeing _tester memory");
free(_tester); _tester = NULL;
Serial.println("RAM: " + String(freeRam(), DEC));
Serial.println("_tester = " + String((uint16_t)_tester, DEC));
delay(1000); // quick look
}
Je n'ai constaté aucune dégradation de la mémoire vive ou de ma capacité à l'allouer dynamiquement à l'aide de cette méthode. Je dirais donc que c'est un outil viable. FWIW.
Est-ce une très mauvaise idée d'utiliser malloc () et free () avec Arduino?
La reponse courte est oui. Voici les raisons pour lesquelles:
Il s’agit de comprendre ce qu’est une MPU et comment programmer dans les limites des ressources disponibles. L'Arduino Uno utilise un MPU ATmega328p avec une mémoire flash ISP de 32 Ko, une mémoire EEPROM 1024B et une mémoire SRAM de 2 Ko. Ce n'est pas beaucoup de ressources de mémoire.
N'oubliez pas que la SRAM de 2 Ko est utilisée pour toutes les variables globales, les littéraux de chaîne, la pile et l'utilisation possible du segment de mémoire. La pile doit également avoir de la marge pour un ISR.
La disposition de la mémoire est la suivante:
Les ordinateurs de bureau / portables d'aujourd'hui ont plus de 1 000 000 de mémoire. Un espace de pile par défaut de 1 Mo par thread n'est pas rare, mais totalement irréaliste sur un MPU.
Un projet de logiciel intégré doit faire un budget de ressources. Il s'agit d'estimer la latence des ISR, l'espace mémoire nécessaire, la puissance de calcul, les cycles d'instruction, etc. Il n'y a malheureusement pas de repas libres et la programmation embarquée en temps réel difficile est la plus difficile des compétences de programmation à maîtriser.
Ok, je sais que c’est une vieille question, mais plus je lis les réponses, plus je reviens constamment à une observation qui semble importante.
Il semble y avoir un lien avec le problème de Halting de Turing ici. Permettre une allocation dynamique augmente les chances de "mettre un terme", et la question devient donc celle de la tolérance au risque. Bien qu'il soit pratique d'éliminer la possibilité d'un malloc()
échec, etc., cela reste un résultat valable. La question posée par le PO semble concerner uniquement la technique, et oui, les détails des bibliothèques utilisées ou du MPU spécifique importent; la conversation se tourne vers la réduction du risque d’arrêt du programme ou de toute autre fin anormale. Nous devons reconnaître l'existence d'environnements qui tolèrent le risque de manière très différente. Mon projet de passe-temps, qui consiste à afficher de jolies couleurs sur une bande de LED, ne tuera pas quelqu'un si quelque chose d'inhabituel se produit, mais le MCU d'une machine cœur-poumon le fera probablement.
Pour ma bande de LED, peu importe si elle se verrouille, je vais simplement la réinitialiser. Si j'étais sur une machine coeur-poumon artificiel contrôlé par une MCU les conséquences de la bloquer ou ne pas fonctionner sont littéralement la vie et la mort, si la question malloc()
et free()
la possibilité de démontrer M. devrait être divisé entre la façon dont les offres de programme destinés Le fameux problème de Turing. Il peut être facile d’oublier que c’est une preuve mathématique et de nous convaincre que si nous sommes assez intelligents, nous pourrons éviter d’être une victime des limites du calcul.
Cette question devrait avoir deux réponses acceptées, une pour ceux qui sont forcés de cligner des yeux quand ils regardent le problème en face, et une pour tous les autres. Bien que la plupart des utilisations de l'arduino ne soient probablement pas des applications critiques ou des applications vitales, la distinction est toujours présente, quel que soit le MPU que vous codez.
Non, mais ils doivent être utilisés avec beaucoup de soin pour libérer () la mémoire allouée. Je n'ai jamais compris pourquoi les gens disaient que la gestion directe de la mémoire devait être évitée, car elle impliquait un niveau d'incompétence généralement incompatible avec le développement de logiciels.
Disons que vous utilisez votre arduino pour contrôler un drone. Toute erreur dans une partie de votre code pourrait potentiellement la faire tomber du ciel et blesser quelqu'un ou quelque chose. En d'autres termes, si une personne n'a pas les compétences nécessaires pour utiliser malloc, elle ne devrait probablement pas coder du tout, car il existe de nombreux autres domaines dans lesquels de petites anomalies peuvent causer de graves problèmes.
Les bogues causés par malloc sont-ils plus difficiles à détecter et à corriger? Oui, mais c’est plus une question de frustration de la part des codeurs que de prendre des risques. En ce qui concerne les risques, toute partie de votre code peut être égale ou plus risquée que malloc si vous ne prenez pas les mesures qui s’imposent pour vous assurer qu’il est bien fait.