Ist die Verwendung von malloc () und free () eine wirklich schlechte Idee für Arduino?

49

Die Verwendung von malloc()und free()scheint in der Arduino-Welt ziemlich selten zu sein. Es wird viel häufiger, aber immer noch mit Vorsicht, in reinem AVR C verwendet.

Ist es eine wirklich schlechte Idee, malloc()und free()mit Arduino zu verwenden?

Cybergibbons
quelle
2
sonst geht dir sehr schnell der speicher aus, und wenn du weißt, wie viel speicher du verbrauchst, kannst du ihn genauso gut statisch zuweisen
ratschenfreak
1
Ich weiß nicht, ob es schlecht ist , aber ich denke, es wird nicht verwendet, weil Ihnen für die meisten Skizzen fast nie der Arbeitsspeicher ausgeht und es nur eine Verschwendung von Flash und wertvollen Taktzyklen ist. Vergessen Sie auch nicht den Gültigkeitsbereich (obwohl ich nicht weiß, ob dieser Speicherplatz noch für alle Variablen reserviert ist).
Anonymous Penguin
4
Wie immer lautet die richtige Antwort "es kommt darauf an". Sie haben nicht genügend Informationen angegeben, um sicher zu sein, ob die dynamische Zuordnung für Sie geeignet ist.
WineSoaked

Antworten:

40

Meine allgemeine Regel für eingebettete Systeme ist, nur malloc()große Puffer und nur einmal zu Beginn des Programms, z setup(). B. in . Das Problem tritt auf, wenn Sie Speicher zuordnen und freigeben. Während einer langfristigen Sitzung wird der Speicher fragmentiert, und schließlich schlägt eine Zuweisung fehl, weil ein ausreichend großer freier Bereich fehlt, obwohl der gesamte freie Speicher für die Anforderung mehr als ausreichend ist.

(Historische Perspektive, bei Nichtinteresse überspringen): Abhängig von der Loader-Implementierung ist der einzige Vorteil der Laufzeitzuweisung gegenüber der Zuweisung zur Kompilierungszeit (initialisierte Globals) die Größe der Hex-Datei. Wenn eingebettete Systeme mit Standardcomputern mit vollständig flüchtigem Speicher erstellt wurden, wurde das Programm häufig von einem Netzwerk oder einem Instrumentierungscomputer auf das eingebettete System hochgeladen, und die Upload-Zeit war manchmal ein Problem. Das Auslassen von Puffern mit Nullen aus dem Bild kann die Zeit erheblich verkürzen.)

Wenn ich in einem eingebetteten System eine dynamische Speicherzuweisung benötige, ordne ich im Allgemeinen malloc()oder vorzugsweise statisch einen großen Pool zu und teile ihn in Puffer fester Größe (oder einen Pool mit jeweils kleinen und großen Puffern) und teile meine eigene Zuweisung / Freigabe aus diesem Pool. Dann wird jede Anforderung nach einer beliebigen Speichermenge bis zur festgelegten Puffergröße mit einem dieser Puffer berücksichtigt. Die aufrufende Funktion muss nicht wissen, ob sie größer als angefordert ist, und indem wir das Teilen und erneutes Kombinieren von Blöcken vermeiden, lösen wir die Fragmentierung. Natürlich kann es immer noch zu Speicherverlusten kommen, wenn das Programm Fehler zuweist / aufhebt.

JRobert
quelle
Ein weiterer historischer Hinweis führte schnell zum BSS-Segment, das es einem Programm ermöglichte, seinen eigenen Speicher für die Initialisierung auf Null zu setzen, ohne die Nullen während des Programmladens langsam zu kopieren.
RSAXVC
16

In der Regel vermeiden Sie beim Schreiben von Arduino-Skizzen die dynamische Zuordnung (sei es mit mallocoder newfür C ++ - Instanzen). Die Benutzer verwenden eher globale staticoder lokale (Stapel-) Variablen.

Die Verwendung der dynamischen Zuordnung kann zu mehreren Problemen führen:

  • Speicherverluste (wenn Sie einen Zeiger auf einen zuvor zugewiesenen Speicher verlieren oder eher vergessen, den zugewiesenen Speicher freizugeben, wenn Sie ihn nicht mehr benötigen)
  • Heap-Fragmentierung (nach mehreren malloc/ freeAufrufen), bei der der Heap größer wird als der aktuell zugewiesene Speicher

In den meisten Situationen, mit denen ich konfrontiert war, war die dynamische Zuweisung entweder nicht erforderlich oder konnte mit Makros wie im folgenden Codebeispiel vermieden werden:

MySketch.ino

#define BUFFER_SIZE 32
#include "Dummy.h"

Dummy.h

class Dummy
{
    byte buffer[BUFFER_SIZE];
    ...
};

Ohne #define BUFFER_SIZE, wenn wir wollten DummyKlasse eine nicht fest haben bufferGröße, hätten wir die dynamische Zuordnung wie folgt verwenden:

class Dummy
{
    const byte* buffer;

    public:
    Dummy(int size):buffer(new byte[size])
    {
    }

    ~Dummy()
    {
        delete [] bufer;
    }
};

In diesem Fall haben wir mehr Optionen als im ersten Beispiel (z. B. verwenden Sie unterschiedliche DummyObjekte mit unterschiedlicher bufferGröße für jedes), es können jedoch Probleme mit der Heap-Fragmentierung auftreten.

Beachten Sie, dass ein Destruktor verwendet wird, um sicherzustellen, dass der dynamisch zugewiesene Speicher bufferfreigegeben wird, wenn eine DummyInstanz gelöscht wird.

jfpoilpret
quelle
14

Ich habe mir den malloc()von avr-libc verwendeten Algorithmus angesehen , und es scheint einige Verwendungsmuster zu geben, die im Hinblick auf die Heap-Fragmentierung sicher sind:

1. Ordnen Sie nur langlebige Puffer zu

Damit meine ich: all das, was Sie zu Beginn des Programms benötigen, zuzuteilen und niemals freizugeben. In diesem Fall können Sie natürlich auch statische Puffer verwenden ...

2. Ordnen Sie nur kurzlebige Puffer zu

Das heißt, Sie geben den Puffer frei, bevor Sie etwas anderes zuweisen. Ein vernünftiges Beispiel könnte so aussehen:

void foo()
{
    size_t size = figure_out_needs();
    char * buffer = malloc(size);
    if (!buffer) fail();
    do_whatever_with(buffer);
    free(buffer);
}

Befindet sich kein Malloc im Inneren do_whatever_with()oder gibt diese Funktion alle zugewiesenen Elemente frei, sind Sie vor Fragmentierung geschützt.

3. Geben Sie immer den zuletzt zugewiesenen Puffer frei

Dies ist eine Verallgemeinerung der beiden vorhergehenden Fälle. Wenn Sie den Heap wie einen Stapel verwenden (last in ist first out), verhält er sich wie ein Stapel und nicht wie ein Fragment. Es ist zu beachten, dass es in diesem Fall sicher ist, die Größe des zuletzt zugewiesenen Puffers mit zu ändern realloc().

4. Ordnen Sie immer die gleiche Größe zu

Dies verhindert keine Fragmentierung, ist jedoch in dem Sinne sicher, dass der Heap nicht größer wird als die maximal verwendete Größe. Wenn alle Puffer dieselbe Größe haben, können Sie sicher sein, dass der Steckplatz bei jeder Freigabe für spätere Zuweisungen verfügbar ist.

Edgar Bonet
quelle
1
Muster 2 sollte vermieden werden, da es Zyklen für malloc () und free () hinzufügt, wenn dies mit "char buffer [size]" möglich ist. (in C ++). Ich möchte auch das Anti-Pattern "Never from a ISR" hinzufügen.
Mikael Patel
9

Die Verwendung der dynamischen Zuordnung (über malloc/ freeoder new/ delete) ist an sich nicht schlecht. Tatsächlich ist es für so etwas wie die Verarbeitung von Zeichenfolgen (z. B. über das StringObjekt) oft sehr hilfreich. Das liegt daran, dass in vielen Skizzen mehrere kleine Zeichenfolgenfragmente verwendet werden, die schließlich zu einer größeren kombiniert werden. Wenn Sie die dynamische Zuordnung verwenden, können Sie nur so viel Speicher verwenden, wie Sie jeweils benötigen. Im Gegensatz dazu kann die Verwendung eines statischen Puffers mit fester Größe für jeden Puffer viel Speicherplatz verschwenden (was dazu führt, dass der Speicher viel schneller ausgeht), obwohl dies ausschließlich vom Kontext abhängt.

Bei alledem ist es sehr wichtig, sicherzustellen, dass die Speichernutzung vorhersehbar ist. Wenn Sie der Skizze erlauben, abhängig von den Umständen zur Laufzeit (z. B. Eingabe) eine beliebige Menge an Speicher zu verwenden, kann dies früher oder später leicht zu Problemen führen. In einigen Fällen ist dies möglicherweise absolut sicher, z. B. wenn Sie wissen, dass die Verwendung niemals zu viel ergibt. Skizzen können sich jedoch während des Programmiervorgangs ändern. Eine früh gemachte Annahme könnte vergessen werden, wenn etwas später geändert wird, was zu einem unvorhergesehenen Problem führt.

Aus Gründen der Robustheit ist es normalerweise besser, mit Puffern mit fester Größe zu arbeiten und die Skizze so zu gestalten, dass sie von Anfang an explizit mit diesen Grenzwerten arbeitet. Das bedeutet, dass zukünftige Änderungen an der Skizze oder unerwartete Laufzeitumstände hoffentlich keine Speicherprobleme verursachen sollten.

Peter Bloomfield
quelle
6

Ich bin nicht einverstanden mit Leuten, die denken, man sollte es nicht benutzen oder es ist im Allgemeinen unnötig. Ich glaube, es kann gefährlich sein, wenn Sie nicht genau wissen, wie es aussieht, aber es ist nützlich. Es gibt Fälle, in denen ich die Größe einer Struktur oder eines Puffers (zur Kompilier- oder Laufzeit) nicht kenne (und auch nicht wissen sollte), insbesondere wenn es um Bibliotheken geht, die ich in die Welt versende. Ich bin damit einverstanden, dass Sie beim Kompilieren nur in dieser Größe backen sollten, wenn es sich bei Ihrer Anwendung nur um eine einzige bekannte Struktur handelt.

Beispiel: Ich habe eine serielle Paketklasse (eine Bibliothek), die Nutzdaten beliebiger Länge aufnehmen kann (Struktur, Array von uint16_t usw.). Am Ende dieser Klasse teilen Sie der Packet.send () -Methode einfach die Adresse des zu sendenden Objekts und den HardwareSerial-Port mit, über den Sie es senden möchten. Auf der Empfängerseite benötige ich jedoch einen dynamisch zugewiesenen Empfangspuffer, um die eingehende Nutzlast zu speichern, da diese Nutzlast zu jedem Zeitpunkt eine andere Struktur haben kann, zum Beispiel abhängig vom Status der Anwendung. WENN ich jemals nur eine einzelne Struktur hin und her sende, würde ich den Puffer einfach auf die Größe bringen, die er zum Zeitpunkt der Kompilierung haben muss. Aber in dem Fall, dass Pakete im Laufe der Zeit unterschiedlich lang sein können, sind malloc () und free () nicht so schlecht.

Ich habe tagelang Tests mit dem folgenden Code ausgeführt und ihn in einer Endlosschleife laufen lassen, und ich habe keine Hinweise auf eine Speicherfragmentierung gefunden. Nach dem Freigeben des dynamisch zugewiesenen Speichers kehrt der freie Betrag zu seinem vorherigen Wert zurück.

// 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
}

Ich habe keine Beeinträchtigung des Arbeitsspeichers oder meiner Fähigkeit, ihn mithilfe dieser Methode dynamisch zuzuweisen, festgestellt. Daher würde ich sagen, dass dies ein praktikables Tool ist. FWIW.

StuffAndyMakes
quelle
2
Ihr Testcode entspricht dem Verwendungsmuster 2. Ordnen Sie nur kurzlebige Puffer zu, die ich in meiner vorherigen Antwort beschrieben habe. Dies ist eines der wenigen Verwendungsmuster, von denen bekannt ist, dass sie sicher sind.
Edgar Bonet
Mit anderen Worten, die Probleme treten auf, wenn Sie anfangen, den Prozessor mit einem anderen unbekannten Code zu teilen - genau das Problem, von dem Sie glauben, dass Sie es vermeiden. Wenn Sie möchten, dass etwas immer funktioniert oder beim Verknüpfen nicht funktioniert, nehmen Sie eine feste Zuweisung der maximalen Größe vor und verwenden Sie diese immer wieder, indem Sie sich beispielsweise von Ihrem Benutzer bei der Initialisierung übergeben lassen. Denken Sie daran, dass Sie normalerweise auf einem Chip laufen, auf dem alles in 2048 Byte passen muss - vielleicht mehr auf einigen Platinen, aber vielleicht auch viel weniger auf anderen.
Chris Stratton
@EdgarBonet Ja, genau. Ich wollte nur teilen.
StuffAndyMakes
1
Das dynamische Zuweisen eines Puffers nur in der benötigten Größe ist riskant, da bei anderen Zuweisungen, bevor Sie sie freigeben, Fragmentierung verbleiben kann - Speicher, den Sie nicht wiederverwenden können. Die dynamische Zuweisung ist außerdem mit einem Nachverfolgungsaufwand verbunden. Eine feste Zuordnung bedeutet nicht, dass Sie den Speicher nicht mehrfach verwenden können, sondern nur, dass Sie die Freigabe in das Design Ihres Programms einarbeiten müssen. Bei einem Puffer mit rein lokalem Bereich können Sie auch die Verwendung des Stapels abwägen. Sie haben auch nicht geprüft, ob malloc () fehlschlägt.
Chris Stratton
1
"Es kann gefährlich sein, wenn Sie nicht genau wissen, wie es aussieht, aber es ist nützlich." fasst so ziemlich die gesamte Entwicklung in C / C ++ zusammen. :-)
ThatAintWorking
4

Ist es eine wirklich schlechte Idee, malloc () und free () mit Arduino zu verwenden?

Die kurze Antwort lautet ja. Im Folgenden sind die Gründe aufgeführt, warum:

Es geht darum zu verstehen, was eine MPU ist und wie man innerhalb der Grenzen der verfügbaren Ressourcen programmiert. Das Arduino Uno verwendet eine ATmega328p- MPU mit 32 KB ISP-Flash-Speicher, 1024 KB EEPROM und 2 KB SRAM. Das sind nicht viele Speicherressourcen.

Denken Sie daran, dass der 2-KB-SRAM für alle globalen Variablen, Zeichenfolgenliterale, Stapel und die mögliche Verwendung des Heapspeichers verwendet wird. Der Stack muss auch Headroom für ein ISR haben.

Das Speicherlayout ist:

SRAM-Karte

Heutzutage haben PCs / Laptops mehr als das 1.000.000-fache des Arbeitsspeichers. Ein Standardstapelspeicher von 1 MB pro Thread ist nicht ungewöhnlich, aber auf einer MPU völlig unrealistisch.

Ein eingebettetes Softwareprojekt muss ein Ressourcenbudget aufbringen. Dies ist eine Schätzung der ISR-Latenz, des erforderlichen Speicherplatzes, der Rechenleistung, der Befehlszyklen usw. Leider gibt es keine kostenlosen Mittagessen, und die harte eingebettete Echtzeitprogrammierung ist die am schwierigsten zu beherrschende Programmierfähigkeit.

Mikael Patel
quelle
Amen dazu: "[H] ard Echtzeit-Embedded-Programmierung ist die am schwierigsten zu beherrschende Programmierfähigkeit."
StuffAndyMakes
Ist die Ausführungszeit von malloc immer gleich? Ich kann mir vorstellen, dass Malloc mehr Zeit in Anspruch nimmt, während es im verfügbaren RAM nach einem Steckplatz sucht, der passt. Dies wäre ein weiteres Argument (abgesehen davon, dass der Arbeitsspeicher knapp wird), um unterwegs keinen Speicher zuzuweisen.
Paul
@Paul Die Heap-Algorithmen (malloc und free) haben normalerweise keine konstante Ausführungszeit und sind nicht wiedereintrittsfähig. Der Algorithmus enthält Such- und Datenstrukturen, die bei Verwendung von Threads Sperren erfordern (Concurrency).
Mikael Patel
0

Ok, ich weiß, dass dies eine alte Frage ist, aber je mehr ich die Antworten durchlese, desto mehr komme ich auf eine Bemerkung zurück, die hervorzuheben scheint.

Das haltende Problem ist real

Hier scheint ein Zusammenhang mit Turings Halteproblem zu bestehen. Das Zulassen einer dynamischen Allokation erhöht die Wahrscheinlichkeit des genannten "Anhaltens", sodass die Frage nach der Risikotoleranz gestellt wird. Es ist zwar praktisch, die Möglichkeit des malloc()Scheiterns usw. auszuschalten , aber es ist immer noch ein gültiges Ergebnis. Die Frage, die das OP stellt, scheint sich nur um Technik zu handeln, und ja, die Details der verwendeten Bibliotheken oder der spezifischen MPU spielen eine Rolle. Das Gespräch zielt darauf ab, das Risiko eines Programmstopps oder eines anderen abnormalen Endes zu verringern. Wir müssen das Vorhandensein von Umgebungen erkennen, in denen Risiken sehr unterschiedlich toleriert werden. Mein Hobbyprojekt, hübsche Farben auf einem LED-Streifen anzuzeigen, wird niemanden umbringen, wenn etwas Ungewöhnliches passiert, aber die MCU in einer Herz-Lungen-Maschine wird es wahrscheinlich tun.

Hallo Herr Turing, mein Name ist Hybris

Für meinen LED-Streifen ist es mir egal, ob er blockiert, ich setze ihn einfach zurück. Wenn ich auf eine Herz-Lungen - Maschine durch eine MCU die Folgen davon Einsperren oder andernfalls zu betreiben sind buchstäblich Leben und Tod, so die Frage nach kontrolliert waren malloc()und free()sollte mit der Möglichkeit Spaltung zwischen dem, wie die vorgesehenen Programm befasst sich sein Herr zu demonstrieren Turings berühmtes Problem. Man kann leicht vergessen, dass es sich um einen mathematischen Beweis handelt, und sich davon überzeugen, dass wir es vermeiden können, die Grenzen der Berechnung zu überschreiten, wenn wir nur klug genug sind.

Diese Frage sollte zwei akzeptierte Antworten haben, eine für diejenigen, die gezwungen sind zu blinken, wenn sie The Halting Problem ins Gesicht starren, und eine für alle anderen. Während die meisten Anwendungen des Arduino wahrscheinlich keine geschäftskritischen oder lebenswichtigen Anwendungen sind, ist die Unterscheidung immer noch vorhanden, unabhängig davon, welche MPU Sie codieren.

Kelly S. Französisch
quelle
Ich glaube nicht, dass das Problem des Anhaltens in dieser speziellen Situation auftritt, wenn man bedenkt, dass die Verwendung von Heap nicht unbedingt willkürlich ist. Bei einer genau definierten Verwendung wird die Verwendung des Heapspeichers vorhersehbar "sicher". Der Grund für das Problem des Anhaltens war, herauszufinden, ob bestimmt werden kann, was mit einem notwendigerweise willkürlichen und nicht so genau definierten Algorithmus geschieht. Es gilt wirklich viel mehr für das Programmieren im weiteren Sinne und als solches finde ich es hier nicht besonders relevant. Ich halte es überhaupt nicht für relevant, ganz ehrlich zu sein.
Jonathan Gray
Ich gebe einige rhetorische Übertreibungen zu, aber der springende Punkt ist wirklich, wenn Sie das Verhalten garantieren möchten. Die Verwendung des Heaps birgt ein viel höheres Risiko als die reine Verwendung des Stacks.
Kelly S. French
-3

Nein, aber sie müssen sehr sorgfältig verwendet werden, um zugewiesenen Speicher freizugeben. Ich habe nie verstanden, warum Leute sagen, dass direktes Speichermanagement vermieden werden sollte, da es ein Maß an Inkompetenz impliziert, das im Allgemeinen nicht mit der Softwareentwicklung vereinbar ist.

Sagen wir, Sie verwenden Ihr Arduino, um eine Drohne zu steuern. Jeder Fehler in einem Teil Ihres Codes kann dazu führen, dass er vom Himmel fällt und jemanden oder etwas verletzt. Mit anderen Worten, wenn jemand nicht über die erforderlichen Fähigkeiten verfügt, um malloc zu verwenden, sollte er wahrscheinlich überhaupt nicht codieren, da es so viele andere Bereiche gibt, in denen kleine Fehler schwerwiegende Probleme verursachen können.

Sind die von malloc verursachten Fehler schwerer aufzuspüren und zu beheben? Ja, aber das ist eher eine Frage der Frustration des Programmierers als des Risikos. Was das Risiko betrifft, kann jeder Teil Ihres Codes genauso oder riskanter sein als malloc, wenn Sie nicht die Schritte unternehmen, um sicherzustellen, dass es richtig gemacht wird.

JSON
quelle
4
Es ist interessant, dass Sie eine Drohne als Beispiel verwendet haben. In diesem Artikel ( mil-embedded.com/articles/… ) heißt es: "Aufgrund des Risikos ist die dynamische Speicherzuweisung im sicherheitskritischen eingebetteten Avionik-Code nach dem DO-178B-Standard verboten."
Gabriel Staples
DARPA hat eine lange Tradition darin, es Auftragnehmern zu ermöglichen, Spezifikationen zu entwickeln, die zu ihrer eigenen Plattform passen - warum sollten sie das nicht, wenn es die Steuerzahler sind, die die Rechnung bezahlen. Aus diesem Grund kostet es sie 10 Milliarden US-Dollar, um zu entwickeln, was andere mit 10.000 US-Dollar anfangen können. Hört sich fast so an, als würden Sie den militärischen Industriekomplex als ehrliche Referenz verwenden.
JSON
Die dynamische Zuweisung scheint eine Aufforderung für Ihr Programm zu sein, die im Abschnitt Halteproblem beschriebenen Berechnungsgrenzen aufzuzeigen. Es gibt einige Umgebungen, in denen ein geringes Risiko für ein solches Anhalten besteht, und es gibt Umgebungen (Weltraum, Verteidigung, Medizin usw.), in denen ein kontrollierbares Risiko nicht toleriert wird. scheitern, weil "es sollte funktionieren" nicht gut genug ist, wenn Sie eine Rakete abschießen oder eine Herz- / Lungenmaschine steuern.
Kelly S. Französisch