Realloc verschwendet viel Platz in meiner MCU?

Ich schreibe einen einfachen Aufgabenplaner und verwende die dynamische Speicherzuweisung auf meinem cc430F5137. Ich stimme zu, dass dies keine gute Praxis ist, aber nehmen wir vorerst an, dass es meine Anwendungsanforderung ist, die dynamische Speicherzuweisung zu verwenden.

In meiner OS.c-Datei

Ich habe zwei Strukturen,

typedef struct
{
    task_t task;
    uint8_t next;
    uint8_t prev;
    uint8_t priority;

} info_t;


typedef struct
{
    task_t task;
    uint8_t index;
} index_t;

Größe von info_tist 8 Bytes und Größe von index_tist 6 Bytes.

Ich auch

index_t* m_index;
info_t* m_info;

Dann muss ich die Funktion initialisieren, in der ich das mache

m_info = NULL;
m_index = NULL;

Jetzt habe ich eine Funktion registerTask(& task), die die Adresse der zu planenden Funktion übernimmt. In dieser Funktion tue ich

m_info = realloc(m_info,(num_registered_tasks + 1) * sizeof(*m_info));

und legen Sie dann die Werte von .priority, next,task und prev fest.

Dann mach ich

m_index = realloc(m_index,(num_registered_tasks + 1) * sizeof(*m_index));

und setze die Werte von task und index. und TUnum_registered_tasks++;

Meine Frage ist, wie sich realloc()in dieser Hinsicht verhält.

Angenommen, mein Speicherplatz, First Task ist registriert, also hat er die ersten 8 Bytes für m_info[0]und die nächsten 6 Bytes für m_index[0]. Was passiert nun, wenn meine zweite Aufgabe diese Funktion aufruft? Ich vermute, dass m_info zuerst nach 16 Bytes kontinuierlicher Daten sucht und diese erst nach den ersten 14 Bytes findet, die Adresse von ändert und den m_info[0]Inhalt kopiert und dann hinzufügt m_info[1]. Und wenn m_indexaufgerufen wird, findet es nur 12 Bytes nach diesen (14 + 16) Bytes und Ort m_index[0]und m_index[1]hier.

Wenn dies wahr ist, wird dann mein gesamter bisheriger Speicherplatz verschwendet?

Wenn ich falsch liege, wie funktioniert diese Funktion dann?

Wie kann ich den bisherigen Platz zusätzlich nutzen?

Ich brauche index_tstruct, um eine Art Suchalgorithmus zu implementieren, also ist es auch notwendig

Dies kann sehr implementierungsspezifisch sein. Ja, im Allgemeinen können Sie davon ausgehen, dass der erste Realloc "irgendwo" einen zusammenhängenden Block finden muss, der wahrscheinlich hinter dem zweiten Block liegt. Der vom ursprünglichen ersten Block belegte Platz wird technisch frei, aber was damit passiert, hängt stark von der Implementierung ab. Vielleicht wird es später alleine wiederverwendet (wenn es zufällig groß genug ist), vielleicht wird es mit dem zweiten Block zusammengeführt, wenn letzterer freigegeben wird und sie zusammen wiederverwendet werden.
Warum müssen sich alle Ihre info_t-Strukturen zusammen in einem zusammenhängenden Speicherblock befinden? Sie haben Next- und Prev-Zeiger, sodass nichts Sie daran hindert, individuelle Blöcke für einzelne info_t-Strukturen zuzuweisen und diese Next- und Prev-Zeiger zu verwenden, um Ihre Linked-List zu pflegen.
Da die Anzahl immer gleich ist, gibt es einen Grund, warum es sich um zwei separate Strukturen handelt? Bei einer MCU ohne Cache ist die Referenzlokalität im Allgemeinen weniger wichtig (sie kann helfen, die Adressierung zu vereinfachen). Dies würde die Fragmentierung von diesen Strukturen eliminieren, und ein angepasster Speicherzuordner könnte die Fragmentierung von anderen Zuweisungen begrenzen (z. B. das Platzieren anderer Objekte an der Spitze des Heaps/Bereichs).

Antworten (3)

Die Verwendung der dynamischen Speicherzuweisung auf einer 16-Bit-MCU mit 4 KB RAM ist sehr schlechte Technik.

Nicht so sehr wegen der üblichen Probleme mit Speicherlecks. Nicht so sehr wegen der Fragmentierung des Heap-Speichers. Nicht einmal wegen des ziemlich hohen Ausführungszeitaufwands, der für die Zuordnungsroutinen benötigt wird. Sondern weil es völlig sinnlos ist und keinen Sinn ergibt.

Sie haben einige reale Anforderungen, die angeben, was Ihr Programm tun soll, und basierend auf diesen muss Ihr Programm genau x Bytes RAM verwenden, um das Worst-Case-Szenario zu bewältigen . Sie brauchen nicht weniger, Sie brauchen nicht mehr, Sie brauchen genau die Menge an RAM, die zur Kompilierzeit bestimmt werden kann.

Es macht keinen Sinn, einen Teil der 4 KB zu speichern und sie ungenutzt zu lassen. Wer wird sie benutzen? Ebenso macht es keinen Sinn, mehr Speicher als für das Worst-Case-Szenario benötigt zuzuweisen. Weisen Sie einfach statisch so viel Speicher wie nötig zu, Punkt.

Außerdem haben Sie ein Worst-Case-Szenario mit maximaler Stack-Nutzung, bei dem Sie sich tief in einem Call-Stack befinden und alle aktivierten Interrupts ausgelöst wurden. Dieses Worst-Case-Szenario kann zur Kompilierzeit berechnet oder zur Laufzeit gemessen werden. Eine gute Praxis besteht darin, mehr RAM als für den schlimmsten Fall erforderlich zuzuweisen, um Stapelüberläufe zu verhindern (der Stapel ist eigentlich auch ein dynamischer Speicherbereich). Oder besser gesagt: Verwenden Sie jedes einzelne Byte RAM, das von Ihrem Programm nicht verwendet wird, für den Stack.

Wenn Ihre Anwendung genau 3 KB RAM benötigt, sollten Sie die restlichen 1 KB für den Stack verwenden.

Die dynamische Zuordnung/der Computer-Heap ist für gehostete Anwendungen wie Windows oder Linux vorgesehen, bei denen jeder Prozess über eine feste Menge an RAM verfügt und zusätzlich Heap-Speicher verwenden kann, um so viel RAM zu verwenden, wie in der Hardware vorhanden ist. Es ist nur sinnvoll, die dynamische Zuordnung in solchen komplexen Mehrprozesssystemen zu verwenden, in denen riesige Mengen an verfügbarem RAM vorhanden sind.

In einem MCU-Programm, entweder Bare-Metal- oder RTOS-basiert, müssen Sie sich darüber im Klaren sein, dass Heap-Implementierungen wie folgt funktionieren: Reservieren Sie eine feste Menge von x KB RAM für den Heap. Wenn Sie die dynamische Zuweisung verwenden, erhalten Sie einen Teil dieser festen Speichermenge. Warum also nicht einfach die x kb nehmen, die Sie für die Zuweisung des Heaps verwenden würden, und sie stattdessen zum Speichern Ihrer Variablen verwenden?


Ich habe dies hier mit einer detaillierteren Erklärung und weiteren möglichen Problemen erweitert:
Warum sollte ich in eingebetteten Systemen keine dynamische Speicherzuweisung verwenden?

Ich verstehe den Punkt Danke. Ich brauchte dynamische Speicherzuweisung für einige Benchmarking des Programms. Nicht, dass ich eine kommerzielle Anwendung mache. Ich habe eine gute Problemumgehung dafür gefunden und werde in einer Antwort erwähnen, was ich getan habe.
Die Worst-Case-Speichernutzung bedeutet nicht unbedingt, dass eine statische Zuordnung nur so viel Speicher verwendet. Darüber hinaus kann in einigen Designs die Funktionalität bei hoher Speicherauslastung fallen gelassen werden, wodurch einige Live-Variablen gelöscht und ihr Speicher freigegeben werden können (dasselbe gilt für andere Ressourcen wie die Verarbeitungszeit); Bei variablem Speicherbedarf durch Features kann die dynamische Zuordnung dem Gerät ermöglichen, ein nettes, aber unnötiges Feature unter einem größeren Bereich von Bedingungen beizubehalten. Sogar die Umnutzung eines Speicherblocks (z. B. von einem E/A-Puffer zu einer Sammlung von Strukturen) ist eine begrenzte Form von dynamischem Speicher.
@Hassan Die Verwendung der dynamischen Zuordnung für das Benchmarking ist noch weniger sinnvoll, da die Einführung der dynamischen Zuordnung die Leistung wahrscheinlich drastisch beeinträchtigen wird. Das Benchmarking eingebetteter Systeme erfolgt mit einem Oszilloskop und einem Allzweck-I/O-Pin. Auf diese Weise erhalten Sie äußerst genaue Benchmarking-Zahlen.
@PaulA.Clayton "Funktionalität kann bei hoher Speicherauslastung fallen gelassen werden, wodurch einige Live-Variablen getötet und ihr Speicher freigegeben werden können" Zu welchem ​​​​Zweck? Wofür werden Sie diese Erinnerung verwenden? Entweder weist Ihr Programm genügend Speicher zu, um den schlimmsten Fall zu bewältigen, oder nicht. Wenn Sie den schlimmsten Fall nicht ausführen, können Sie mit dem sogenannten "gespeicherten" Speicher nichts Sinnvolles tun. Planen Sie, dass das Programm diesen Speicher verwendet, um etwas zu tun, das nichts mit seiner vorgesehenen Aufgabe zu tun hat, wenn es nicht das Worst-Case-Szenario ausführt, oder was?
Wie ich geschrieben habe, wird die Funktionalität, die in "nett, aber unnötig" getötet wird, der freigegebene Speicher verwendet, wenn sich die notwendige Funktionalität dem schlimmsten Fall nähert. Ohne dynamische Zuweisung wird möglicherweise ein teureres Gerät benötigt (was es ermöglichen würde , dass die nette Funktionalität zu 100 % der Zeit verfügbar ist, anstatt sagen wir 99 % der Zeit). Ich vermute, dass der Unterschied zwischen statischer (pro Struktur) Worst-Case-Speichernutzung und dynamischer Worst-Case-Speichernutzung eine bedeutendere Überlegung ist, aber die Fähigkeit, die typische Verfügbarkeit wertvoller, nicht kritischer Funktionen zu unterstützen, schien erwähnenswert.
Nehmen wir als vollständig erfundenes Beispiel an, Sie hätten ein Ereignisprotokoll, das fünf Ereignisse ohne optimistische Speichernutzung enthalten könnte, aber mit optimistischer Speichernutzung 90 % der Zeit 15 Ereignisse, 9 % der Zeit 9 Ereignisse und 1 % der Zeit konnten nur vier Veranstaltungen abhalten. Das Erweitern der Protokollkapazität im typischen Fall könnte den Entwicklungsaufwand wert sein und einen Ereigniseintrag in 1 % der Zeit opfern. (Kein gutes Beispiel, da die Tiefe des Ereignisprotokolls wahrscheinlich unter hoher Belastung am wichtigsten wäre, aber das Verbot des dynamischen Speichers hebt die Wahl vollständig auf.)

Dies wird als Speicherfragmentierung bezeichnet.

Es gibt alle möglichen ausgefallenen Algorithmen, um damit umzugehen, aber der Buchhaltungsaufwand macht sie für eine kleine MCU ungeeignet.

Stattdessen können Sie basierend auf Ihrem erwarteten Nutzungsmuster Ihre eigenen Speicherzuweisungsroutinen schreiben. Sie könnten zum Beispiel eine separate Routine für jeden Ausführungs-Thread haben. Wenn Sie garantieren können, dass free() in der entgegengesetzten Reihenfolge von alloc() geschieht, können Sie einen einfachen Zeiger auf das Ende des Speichers verwenden.

Oder Sie könnten eine Indirektionsebene verwenden: Greifen Sie nur über einen Zeiger auf einen Zeiger auf den Speicher zu. Dann können Sie den Speicher frei müllsammeln und die Zeigertabelle so anpassen, dass sie auf den Speicher zeigt, wo immer er endet.

Die beste Antwort für eingebettete Systeme besteht normalerweise darin, die dynamische Speicherzuweisung vollständig zu vermeiden.

Die Nutzung von Nutzungsmustern ist eine Lösung. Eine andere besteht darin, ein Array mit fester Größe zu verwenden und keine weiteren "Aufgaben" -Objekte zu akzeptieren, die möglicherweise dort hineinpassen könnten. Keine Umverteilung - keine Sorgen.
Hallo Danke, das verstehe ich. Meine Aufgaben können auf beliebige Weise frei werden, also kann ich diese Methode nicht haben. Ich war nur neugierig, ob es möglich ist, dass ich den verfügbaren Speicherplatz ansprechen und m_info von Anfang an und m_index irgendwo am Ende zuweisen kann? m_info und m_index sind immer gleich in der Anzahl der Elemente
@scharfer Zahn. Ich stimme zu, aber ich kann dies nicht verwenden. Ich brauche auch dynamische Speicherzuweisung für einige Benchmarks.
@Hassan Okay, eine andere Option. Sie setzen ein ausreichend großes Limit für die Anzahl der Aufgaben, die Sie haben können, und erhalten dann einen großen Block und teilen ihn in kleine Blöcke auf, sodass jeder Block eine einzelne Aufgabe und auch einen booleschen Wert speichern kann, der angibt, ob dort eine Aufgabe vorhanden ist. Wenn Sie also eine neue Aufgabe speichern müssen, finden Sie einfach einen leeren Chunk und legen ihn dort ab.
@sharptooth - Ist diese Methode nicht wirklich nur eine andere Art, ein Array zu beschreiben ... ? :)
@brhans - Ist ein Array nicht nur eine andere Art, den Hauptspeicher zu beschreiben ...? :)
@brhans Ja, es ist ein Array, aber einige Elemente "fehlen".
Es macht keinen Sinn, die dynamische Speicherzuweisung zu verwenden, warum also empfehlen? Ich will nicht unhöflich sein, aber wenn Sie dynamischen Speicher für eine 16-Bit-MCU mit 4 KB RAM verwenden, verwenden Sie nicht den gesunden Menschenverstand und sind sich nicht ganz sicher, was Sie tun, so einfach ist das.

Nach vielen, vielen Kommentaren und Antworten, keinen dynamischen Speicher zu verwenden, fand ich immer noch einen Weg, der mein Problem zur Zeit löste.

Ich habe das in meinem Code geändert

typedef struct
{
    task_t task;
    uint8_t next;
    uint8_t prev;
    uint8_t priority;

} info_t;


typedef struct
{
    task_t task;
    uint8_t index;
} index_t;

typedef struct
{
    info_t m_info;
    info_t m_index;
} combined_t;

combined_t* m_combined;

Dadurch weist nw my realloc() m_combined zusammenhängenden Speicher zu und mein Fragmentierungsproblem ist gelöst. (Nun, ich denke, weil nach der Analyse des Speichers Schritt für Schritt).

Typedefs zum Definieren von Aliasen für Strukturen ist schlechtes Design. Verwenden Sie stattdessen explizite Strukturen.
@Martin Ich bin mir nicht sicher, ob ich deinen Kommentar ganz verstehe. Wollen Sie sagen, das typedef struct { int a; } foo_t; foo_t bar;ist schlechtes Design, während struct foo_s { int a; }; struct foo_s bar;es gutes Design ist? Wenn ja, warum dann?