L'utilisation de malloc () et de free () est-elle une très mauvaise idée sur Arduino?


49

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?


2
Sinon, vous manquerez vraiment de mémoire, sinon, et si vous savez combien de mémoire vous allez utiliser, vous pourriez aussi bien l'attribuer statiquement de toute façon
Ratchet Freak

1
Je ne sais pas si c'est mauvais , mais je pense que ce n'est pas utilisé, car la plupart des esquisses ne manquent presque jamais de RAM. C'est juste un gaspillage de flash et de précieux cycles d'horloge. En outre, n'oubliez pas la portée (bien que je ne sache pas si cet espace est toujours alloué pour toutes les variables).
Anonyme Penguin

4
Comme d'habitude, la bonne réponse est "ça dépend". Vous n'avez pas fourni suffisamment d'informations pour savoir avec certitude si l'allocation dynamique vous convient.
WineSoaked

Réponses:


40

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.


Autre remarque historique, cela a rapidement conduit au segment BSS, ce qui a permis à un programme de mettre à zéro sa propre mémoire pour l’initialisation, sans copier lentement les zéros lors du chargement du programme.
Rsaxvc

16

En règle générale, lors de l'écriture d'esquisses Arduino, vous éviterez l'allocation dynamique (que ce soit avec mallocou newpour les instances C ++), mais les utilisateurs utilisent plutôt des staticvariables globales ou des variables locales (de pile).

L'utilisation de l'allocation dynamique peut entraîner plusieurs problèmes:

  • Fuites de mémoire (si vous perdez un pointeur sur une mémoire que vous avez précédemment allouée, ou plus probable si vous oubliez de libérer la mémoire allouée quand vous n'en avez plus besoin)
  • fragmentation de tas (après plusieurs malloc/ freeappels) où le tas devient plus grand que la quantité réelle de mémoire allouée actuellement

Dans 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 Dummyclasse ait une buffertaille 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 Dummyobjets avec une buffertaille 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 buffersera libérée lorsqu'une Dummyinstance est supprimée.


14

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:

1. Allouer uniquement des tampons de longue durée

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 ...

2. N'attribuez que des tampons de courte durée

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.

3. Toujours libérer le dernier tampon alloué

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().

4. Toujours allouer la même taille

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.


1
Le modèle 2 doit être évité car il ajoute des cycles pour malloc () et free () lorsque cela peut être fait avec "char buffer [size];" (en C ++). J'aimerais également ajouter l'anti-motif "Jamais d'un ISR".
Mikael Patel

9

L'utilisation de l'allocation dynamique (via malloc/ freeou new/ delete) n'est pas fondamentalement mauvaise en soi. En fait, pour quelque chose comme le traitement de chaîne (par exemple via l' Stringobjet), 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.


6

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.


2
Votre code de test est conforme au modèle d'utilisation 2. N'affectez que les tampons de courte durée décrits dans ma réponse précédente. C’est l’un de ces rares modes d’utilisation connus pour être sûrs.
Edgar Bonet

En d'autres termes, les problèmes surgiront lorsque vous commencerez à partager le processeur avec un code inconnu - ce qui est précisément le problème que vous pensez éviter. En règle générale, si vous souhaitez que quelque chose fonctionne toujours ou échoue lors de la liaison, vous attribuez une taille fixe à la taille maximale et vous l'utilisez encore et encore, par exemple en demandant à votre utilisateur de vous le transmettre lors de l'initialisation. Rappelez-vous que vous utilisez généralement une puce sur laquelle tout doit tenir dans 2 048 octets - peut-être plus sur certaines cartes, mais peut-être beaucoup moins sur d'autres.
Chris Stratton

@ EdgarBonet Oui, exactement. Je voulais juste partager.
StuffAndy Fait

1
Allouer dynamiquement un tampon de la taille requise est risqué, car si vous allouiez autre chose avant de le libérer, vous pouvez vous retrouver avec une fragmentation - une mémoire que vous ne pouvez pas réutiliser. En outre, l'allocation dynamique entraîne une surcharge de suivi. Une allocation fixe ne signifie pas que vous ne pouvez pas utiliser plusieurs fois la mémoire, cela signifie simplement que vous devez intégrer le partage dans la conception de votre programme. Pour un tampon avec une étendue purement locale, vous pouvez également peser l'utilisation de la pile. Vous n'avez pas vérifié la possibilité d'échec de malloc () non plus.
Chris Stratton

1
"Cela peut être dangereux si vous ne connaissez pas les tenants et les aboutissants de celui-ci, mais c'est utile." résume à peu près tout le développement en C / C ++. :-)
ThatAintWorking

4

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:

Carte SRAM

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.


Amen à cela: "La programmation embarquée en temps réel est la plus difficile des compétences de programmation à maîtriser."
StuffAndyMakes

Le temps d'exécution de malloc est-il toujours le même? Je peux imaginer que malloc prenne plus de temps à chercher dans le bélier disponible un emplacement qui lui convient? Ce serait encore un autre argument (mis à part le manque de mémoire vive) pour ne pas allouer de la mémoire lors de vos déplacements?
Paul

@Paul Les algorithmes de tas (malloc et free) ne sont généralement pas des temps d'exécution constants et ne sont pas réentrants. L'algorithme contient des structures de recherche et de données qui nécessitent des verrous lors de l'utilisation de threads (simultanéité).
Mikael Patel

0

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.

Le problème de l'arrêt est réel

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.

Bonjour M. Turing, mon nom est Hubris

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.


Je ne pense pas que le problème de Halting s’applique dans cette situation particulière, compte tenu du fait que l’utilisation du tas n’est pas nécessairement arbitraire. Si elle est utilisée de manière bien définie, l’utilisation du tas devient de manière prévisible "sûre". Le problème du problème Halting était de savoir s'il était possible de déterminer ce qu'il advient d'un algorithme nécessairement arbitraire et pas aussi bien défini. Cela s’applique beaucoup plus à la programmation dans un sens plus large et, en tant que tel, j’estime qu’elle n’est pas particulièrement pertinente ici. Je ne pense même pas qu'il soit pertinent d'être tout à fait honnête.
Jonathan Gray le

J'admettrai certaines exagérations rhétoriques, mais le fait est que si vous voulez garantir le comportement, utiliser le tas implique un niveau de risque beaucoup plus élevé que de s'en tenir à la pile.
Kelly S. French le

-3

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.


4
Il est intéressant que vous utilisiez un drone comme exemple. Selon cet article ( mil-embedded.com/articles/… ), "En raison de son risque, l'allocation dynamique de mémoire est interdite, en vertu de la norme DO-178B, dans du code d'avionique embarqué essentiel pour la sécurité."
Gabriel Staples

La DARPA permet depuis longtemps aux entrepreneurs de développer des spécifications qui s’adaptent à leur propre plate-forme. Pourquoi ne le feraient-ils pas alors que ce sont les contribuables qui paient la facture. C'est pourquoi il leur en coûte 10 milliards de dollars pour développer ce que d'autres peuvent faire avec 10 000 dollars. On dirait presque que vous utilisez le complexe militaro-industriel comme une référence honnête.
JSON

L'allocation dynamique semble être une invitation pour votre programme à démontrer les limites de calcul décrites dans le Problème d'arrêt. Certains environnements peuvent gérer un faible risque d'arrêt et il existe des environnements (espace, défense, médical, etc.) qui ne tolèrent aucun risque contrôlable. Ils interdisent donc les opérations qui ne devraient pas. échouer parce que «ça devrait marcher» n’est pas assez bon quand vous lancez une fusée ou contrôlez une machine cœur / poumon.
Kelly S. French
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.