Auf einem Mikrocontroller will ich einem Array mehrere Objekte anlegen. Die sollen dann mit einer "exec" Funktion benutzt werden. Später will ich diese dann löschen und wieder neu anlegen. Wie verhindere ich ein Speicherleck?
Hier mal ein Beispiel für die Objekte:
1
#include<iostream>
2
3
classCounter{
4
private:
5
intcount;
6
7
public:
8
Counter():count(0){}// Constructor initializes count to 0
9
10
voidexec(){
11
count++;
12
}
13
14
intgetCount()const{
15
returncount;
16
}
17
};
18
19
classCounter_IF{
20
private:
21
CountercounterInstance;// Instance of Counter
22
23
public:
24
voidexec(){
25
counterInstance.exec();// Call exec on the Counter instance
26
}
27
28
intgetCount()const{
29
returncounterInstance.getCount();// Retrieve count from the Counter instance
30
}
31
};
32
33
intmain(){
34
Counter_IFcounters[5];// Array of 5 Counter_IF instances
So bald main verlassen wird, wird Counter_IF aufgelöst.
Danke. Mein Beispiel war etwas irreführend. Hier die Main noch mal neu.
Die Liste für die Counter_IF ist 10 Einträge lang.
Zuerst werden 5 Counter erzeugt und dann laufen lassen.
In der zweiten Runde werden 8 Counter erzeugt. Die 5 vorher benutzen werden eigentlich nicht mehr benötigt,aber ich schätze, sie hängen immer noch im Speicher herum. Wie kann man das feststellen?
Die 5 vorher benutzen werden eigentlich nicht mehr benötigt,aber ich
schätze, sie hängen immer noch im Speicher herum.
Brauchst du den Speicher jemals für was anderes? Spricht was dagegen einfach fix 10 Instanzen zu erzeugen und einfach nur die jeweils benötigte Anzahl zu benutzen?
Wenn man wirklich dynamisch allokiert (mit new/malloc) muss man auch wieder freigeben (delete/free). Aber das ist auf Mikrocontrollern selten sinnvoll. Wenn dann sollte man zB std::unique_ptr nutzen, was dieses sicherer kapseln. Leider steht das auf dem AVR-GCC nicht zur Verfügung.
Um das genauer zu untersuchen, brauch man wohl erst einmal einen
"Speicherverbrauchsmesser".
Das ist eigentlich selten nötig. SBRK wird eigentlich nie reduziert, wozu auch, es gibt ja keine anderen Programme die den Speicher dann bekommen könnten. Du kannst nicht so einfach die aktuelle Speichernutzung abfragen, die zeichnet C/C++ nicht explizit auf, weil ineffizient.
Dein Buffer liegt auf dem Stack, der wird natürlich von SBRK überhaupt nicht erfasst. Den Stack kann man zwar auch abfragen, aber muss das sein...
Gerade auf kleinen Mikrocontrollern ist dynamische Speicherverwaltung selten sinnvoll!
Wer new sagt muss auch delete aufrufen. Und damit das immer passiert, nutzt man eine Klasse zur Arrayverwaltung und den Destruktor darin.
Das sind aber Grundlagen der objektorientierten Programmierung,
wegen der Freigabeproblematik in Standard-C insbesondere bei exceptions (oder poor mans exception: ljmp) hat man ja Klassen mit Destruktoren als Erweiterung der reinen Datenstrukturen struct überhaupt erst eingeführt.
Ein delete ist nicht Pflicht, wenn man Objekte eben Pseudo-statisch anlegt. Das macht auch auf μC Sinn, z.B. wenn man eine Konfigurationsdatei einliest und anhand derer Objekte anlegt. Dann muss man natürlich auswerten ob das Erfolgreich war und Fehler anzeigen können.
Bei dem Counter Beispiel könnte man auch ein used flag verwenden. Erstmal suchen ob einer frei ist und den dann benutzen, wenn nicht neuen anlegen. Wird auch billiger sein als delete.
Implementieren des Designs + Review machen, MISRA plus es gibt andere statische und dynamische code checks.
Unbedingt tests schreiben. Unit oder Module test, wie man es nennen mag. Dann noch integration und system tests auch.
Wenn man das alles gemacht hat, dann sind vermutlich alle Speicherlecks vermieden. Wenn da doch eins entsteht, dann muss man gucken wo, wieso, weshalb. Und auf den Fall den Prozess erweitern.
Brauchst du den Speicher jemals für was anderes? Spricht was dagegen
einfach fix 10 Instanzen zu erzeugen und einfach nur die jeweils
benötigte Anzahl zu benutzen?
Fixe Instanzen geht leider nicht.
Andras H. (andras_h)
Wie man einen Speicherleck vermeidet?
Requirements schreiben + Review machen
Geil. Ich wollte ja schon immer "Requirements" schreiben. Das hilft ja bekanntlich bei jeder Entwicklung, wie konnte ich das übersehen ;-)
Requirements:
Ein Mikrocontroller soll über die serielle Schnittstelle Kommandos empfangen können.
Bei jedem Kommando "counter" soll die Instanz eines Zähler erzeugt werden. Verweise auf die Instanzen sollen in ein Array eingetragen werden.
Beim Empfang des Kommandos "run" soll die Exec-Function aller im Array befindlichen Objekte aufgerufen werden.
Es sollen so viele Objekte erzeugt werden können, wie in den Speicher passen
Empfängt der Mikrocontroller den Befehl 'delete' über die serielle Schnittstelle, sollen alle Objekte gelöscht und er Speicher wieder frei gegeben werden.
Schritt 2 soll wiederholt werden können.
Es soll möglich sein, das Programm zu erweitern für neue Kommados z.B. "generator" (Signalgenerator)
Kein new verwendet. C++ hat Container, die übernehmen das. Wenn's
wirklich keine vollständige C++-stdlib gibt, das ganze Konzept
überdenken.
Ich will das Ganze auf einem Atmega328 mit 2K RAM und 32K Flash verwenden. Der Compiler ist AVR-GCC und ich weiß nicht, ob die stdlib für so kleine Controller passt.
Ich habe mal den Klassen-Code von "blink without delay" für einen Test verwendet.
Über die serielle Schnittstelle kann man die Kommandos
h: help
d: delete
n: new
senden.
Es werden jeweils 2 mit unterschiedlich blinkende LEDs erzeugt.
Wie man aber sieht, wächst der Speicher nach einem delete und dann new langsam an.
1
object: 0 address: 545
2
object: 1 address: 563
3
delete
4
create new list
5
object: 0 address: 581
6
object: 1 address: 599
Wen ich es richtig sehe, werden wohl für jedes Object 36 Bytes verbraucht.
Idx = 0;
for (int n = 0; n < Idx; n++) delete(Liste[n]);
Hâh ?
Die Idee hier ist:
In der Liste befinden sich die Pointer auf die Objekte.
Mittels List[0] ist z.B. der Pointer auf das Object 0. Mit delete[0] wird der Destructor des Objects aufgerufen in der Hoffnung, dass dann der durch das Objekt belegte Speicher freigegeben wird.
Bei jedem Kommando "counter" soll die Instanz eines Zähler erzeugt
werden.
Und es muss wirklich eine neue Instanz auf dem Heap angelegt werden? Wo merkt der Nutzer einen Unterschied, wenn in Wirklichkeit eine Instanz auf .data recycelt wird? Das ist sowieso der selbe SRAM, nur die Adressen werden anders bestimmt. Von außen ist kein Unterschied sichtbar, warum beziehen sich deine Anforderungen auf Implementationsdetails?
Ich will das Ganze auf einem Atmega328 mit 2K RAM und 32K Flash
verwenden.
Das ist sehr knapp für dynamische Speicherverwaltung mit new/malloc! Wenn du das wirklich unbedingt brauchst, solltest du einen größeren Controller nehmen. Am Besten gleich einen Cortex-M, dort sind Adressberechnungen und damit auch dynamische Speicherverwaltungen effizienter.
Wen ich es richtig sehe, werden wohl für jedes Object 36 Bytes
Das kann man übrigens auch super mit sizeof() abfragen.
Ich vermute dir fehlt es ein bisschen an Erfahrungsschatz und Wissen über C++ um wirklich einzuschätzen, was für eine Art der Speicherverwaltung du brauchst. Vielleicht erläuterst du mal genau warum das so ist:
Ich glaube so etwas wie std::inplace_vector wäre richtig für dich - es wird ein fixer Speicherbereich reserviert und es wird gemerkt, welche davon in Verwendung sind. Das ist aber erst ab C++26 verfügbar. Das geht nur dann nicht, wenn du den Speicher für eine ganz andere Datenstruktur brauchst, während keine Counter existieren. Wenn du lediglich verschiedene Counter-Typen unterstützten möchtest, kannst du auch std::variant bemühen.
Im Anhang ist eine Quick-n-Dirty Implementation von std::inplace_vector. Schau doch mal ob das für dich geeignet wäre. Das dort auftauchende "new" ist ein placement-new, d.h. es wird einfach nur der Konstruktor auf einem vorgegebenen Speicher aufgerufen, aber kein dynamischer Speicher benutzt. Du kannst neue Elemente zum Ende hinzufügen bis der Speicher voll ist, das letzte Element oder alle Elemente löschen, und die Größe abfragen. Es wird immer korrekt der Konstruktor/Destruktor der Objekte aufgerufen.
Grundsätzlich sollte man solche Datenstrukturen nicht auf dem Stack anlegen wie bei dir, daher habe ich es bei mir als globale Variable deklariert.
Wen ich es richtig sehe, werden wohl für jedes Object 36 Bytes
Das kann man übrigens auch super mit sizeof() abfragen.
Naturlich nicht.
sizeof nennt die deklarierte Grösse der Datenstruktur in bytes, aber weiss weder auf welche Granularität aufgerundet wird noch welche Verwaltungsinformation zusätzlich auf dem heap gespeichert wird, hat also keine Ahnung um wie viel der heap bei jedem alloc anwachsen wird.
Ich vermute dir fehlt es ein bisschen an Erfahrungsschatz
aber
weiss weder auf welche Granularität aufgerundet wird noch welche
Verwaltungsinformation zusätzlich auf dem heap gespeichert wird
Richtig, ich bitte um Verzeihung, es war ungenau formuliert. Ich wollte natürlich sagen, dass sizeof() die Größe des Objekts inklusive Padding (für das Alignment von Arrays), liefert, weil ich vermutet hatte, dass Christoph dies wissen möchte, aber eben exklusive des Verwaltungsoverheads.
Ja, trotz vieler Jahre Interneterfahrung vergesse ich immer wieder, Dinge extrem penibel zu formulieren, damit mir nicht wieder Unwissen vorgeworfen wird. Werde ich wohl nie lernen.
sizeof nennt die deklarierte Grösse der Datenstruktur in bytes, aber
weiss weder auf welche Granularität aufgerundet wird
Beim AVR wird wie bei den meisten 8-Bit Architekturen nicht aufgerundet.
noch welche Verwaltungsinformation zusätzlich auf dem heap gespeichert wird
also keine Ahnung um wie viel der heap bei jedem alloc anwachsen wird.
Das ist für eine Abschätzung des zu erwartenden Speicherverbrauchs im Kontext der hier diskutierten Aufgabenstellung vernachlässigbar. Sizeof ist für eine grobe Abschätzung völlig ausreichend.
Beim AVR wird wie bei den meisten 8-Bit Architekturen nicht aufgerundet.
Bei jedem Heap wird mindestens auf die Grösse der freelist-Pointer aufgerundet, also 2 oder 4 byte, dazu kommt die Grössenangabe des Blocks, also weitere 2 byte.
Ein malloc(1) benötigt also zumindest 4 bytes vom Heap.
Du scheinst mit Compilerentwicklung und heap Struktur noch nie was zu tun gehabt zu haben.
Vielen Dank für deine ausführliche Antwort und deinen Code-Beitrag :-)
Und es muss wirklich eine neue Instanz auf dem Heap angelegt werden?
Ich vermute ja, weil die Kommandos, was angelegt werden soll, ja dynamisch über die serielle Schnittstelle (oder falls es auf dem PC läuft, von einem Konfig-File) kommen.
Das ganze lasse ich auf folgenden Systemen laufen:
Arduino Nano (Atmega328), 2k Ram, 32kFlash
gcc version 5.4.0 (GCC)
gcc version 7.3.0 (GCC)
Pipico2, 520k Ram
gcc version 14.2.0 (GCC)
PC, 16GB Ram
gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04)
Der Atmega hat den Vorteil, dass man ultraschnell kompilieren und testen kann. Außerdem zwingen die beschränkten Resourcen zur Optimierung von Speicherverbrauch und Rechenzeit.
Die Compilierung und der Donwload auf dem PiPico dauert ewig, deshalb nutze ich den immer erst, wenn ein Modul fertig ist. Für manche Anwendungen brauche ich aber die Geschwindigkeit und vielleicht auch den Speicher.
Den PC nutze ich zur Entwicklung, insbesondere zum debuggen. Die Arduino-Funktionen wie "digitalWrite" sind "gestupped".
Ich vermute dir fehlt es ein bisschen an Erfahrungsschatz und Wissen
über C++ um wirklich einzuschätzen, was für eine Art der
Speicherverwaltung du brauchst.
C++ verwende ich eher in rudimentärer Form.
Die Funktionalität, die ich von C++ brauche ist:
Instanzen von Klassen anlegen, die von einer übergeordneten Klasse abgeleitet sind, die im wesentlichen eine "init" Funktion und eine "exec" Funktion hat. Die Instanzen werden in einem Array eingetragen und dann in einem Rutsch alle "exec" Funktionen der im Array befindlichen Klassen aufgerufen. Das gesamte Array und die Klassen müssen in einem Rutsch gelöscht werden können, da bei entsprechenden Kommandos über die serielle Schnittstelle alles neu angelegt werden muss.
Deshalb braucht das System auch keinen "garbage collector" weil nicht einzelne Instanzen sondern immer das gesamte Array gelöscht werden und die Instanzen gelöscht werden müssen, die vermutlich schön hintereinander im Speicher platziert sind.
Die Funktionalität, die ich von C++ brauche ist:
Instanzen von Klassen anlegen, die von einer übergeordneten Klasse
abgeleitet sind, die im wesentlichen eine "init" Funktion und eine
"exec" Funktion hat.
Also einfach nur Polymorphie. Dafür brauchst du immer noch keine dynamische Speicherverwaltung. Du kannst einfach meine o.g. InplaceVector Klasse mit std::variant kombinieren, dann kannst du in jedes Element eine der abgeleiteten Klassen packen, also z.B. als InplaceVector<std::variant<Derived1, Derived2, Derived3>>. Du kannst für jedes Element beliebig entscheiden welche Klasse drin angelegt ist.
Du brauchst dann noch nichtmal die gemeinsame Basisklasse. Über std::visit kannst du "init" (reicht nicht der Konstruktor?) und "exec" aufrufen. Du kannst die gemeinsame Basis trotzdem implementieren und dann noch pro Element einen Pointer darauf behalten um die Funktionen ohne std::visit aufzurufen, das ist dann aber etwas doppelt gemoppelt...
Deshalb braucht das System auch keinen "garbage collector"
Den braucht man sowieso nie, auch bei wesentlich komplexeren Strukturen, sofern mann seine Daten immer korrekt freigibt, via delete/free/std::unique_ptr/std::shared_ptr. C++ hat auch iA gar keinen GC. Ein GC macht es nur einfacher, hat aber auch Overhead. Ist auf einem kleinen Mikrocontroller eh unpraktikabel.
Letzten Endes läuft es doch darauf raus dass du auch bei der dynamischen Geschichte eine maximale Menge festlegen musst. Denn einer zu viel und deinem uC fliegt der Deckel weg.
Außerdem musst du genau für diesen Fall den Speicher vorhalten. Overhead durch new/delete wurde ja auch schon genannt.
Ich würde vermutlich eine feste Menge instanziieren. Die werden wenn sie gebraucht werden aktiviert/parametriert. Die arbeitest du dann ab.
Beim Löschen werden sie nur initialisiert/deaktiviert.
Danke. Mein Beispiel war etwas irreführend. Hier die Main noch mal neu.
Die Liste für die Counter_IF ist 10 Einträge lang.
Zuerst werden 5 Counter erzeugt und dann laufen lassen.
In der zweiten Runde werden 8 Counter erzeugt. Die 5 vorher benutzen
werden eigentlich nicht mehr benötigt,aber ich schätze, sie hängen immer
noch im Speicher herum. Wie kann man das feststellen?
Nimm einen std::vector
Der alloziert nur speicher wenn er waechst. Du kannst neue counter mit der clear()/resize() combo erzeugen. Der vector arbeitet mit placement new, d.h. wenn der vorhandene Speicher ausreicht, wird keiner alloziert.
Braucht aber trotzdem malloc/new und zieht damit eine Menge Code aus der Standard Bibliothek rein. Was macht man mit dem freigewordenen Speicher wenn man den std::vector leert? Nichts? Dann kann man auch einen in-place-vector nehmen. Ganz nebenbei müssen die Elemente dafür auch nicht kopierbar/move-bar sein.
Ist einfach unnötige Verschwendung von Flash, Rechenzeit und RAM.
gibt es Geld zurueck fuer nicht genutzen Flash?
Was meinst du wie komplex der std::vector code hier ist, verglichen mit deinem inplace vector?
Was ist der Vorteil gegenüber der Lösung, den Speicher einfach direkt
vom Linker allokieren zu lassen?
Standard Library Komponente. Einziger Nachteil ist dass der Speicherbedarf nicht zu compile-zeit ermittelt wird. Ansonsten identisch.
Aber wie gezeigt kann man sich den sehr leicht selbst implementieren und
hat dann minimalen Aufwand in RAM, Flash, Rechenzeit.
Leicht? Fuer einen Anfaenger verstaendlich? Der RAM, Flash und Rechenzeit vergleich hinkt.
Hmm: https://godbolt.org/z/zecW614Kd
Okay, wenn man nie Elemente hinzufügt, braucht man keine move/copy
Konstruktoren/Zuweisungsoperatoren. Aber ich glaub das hilft hier nicht.
Was meinst du wie komplex der std::vector code hier ist, verglichen mit
deinem inplace vector?
Größenordnungen komplexer. Allein schon für malloc. Ein Quick-And-Dirty Vergleich für Cortex-M4 (vector gibt's ja nicht beim AVR-GCC), mit einem dummy-print statt iostream um dessen Overhead nicht mitzunehmen:
1
text data bss dec hex filename
2
9060 92 1908 11060 2b34 vector.elf
3
1004 4 1652 2660 a64 inplace.elf
Der statisch allokierte RAM-Platz ist für den in-place vector kleiner als allein nur der Overhead für die Implementation vom std::vector auf dem statisch allokierten RAM (.data, .bss).
So schlimm ist es nicht. Ein Anfänger könnte es auch ohne Template machen, dann bleibt nicht mehr viel übrig. Den iterator kann man sich sparen und immer "zu Fuß" iterieren.
Weiss nicht was du damit zum Ausdruck bringen willst, aber:
Diese Klasse hat den impliziten Copy/Move Konstruktur/Zuweisungsoperator. Aber den hat man nicht immer, in meinem Beispiel hab ich diese explizit "deleted". Oft es es auch nicht praktikabel, Copy/Move zu implementieren. Und dann kann man einen vector, der diese Klasse enthält, zwar anlegen, aber man kann keine Elemente hinzufügen.
/home/christoph/Entwicklung/250204_GraphProg2025/ArduinoVM/TEST_objectDelete/TEST_objectDelete.ino:11:11: warning: inline function 'constexpr InplaceVector<T, N>::Container::Container() [with T = Counter_IF; unsigned int N = 8]' used but never defined
Ja, der AVR-GCC liefert keine C++ Standard-Bibliothek mit. Im Anhang ist eine Variante die die fehlenden Sachen mitliefert. Ist natürlich ein recht fieser Workaround.
constexpr InplaceVector<T, N>::Container::Container()' cannot be
overloaded with 'constexpr InplaceVector<T, N>::Container::Container()'
Ah, die Arduino-DIE verunstaltet, äh, vorverarbeitet den Code und verschluckt sich offenbar am Konstruktor der "Container" Klasse und fügt dort die Prototypen von setup und loop ein. Im angehängten Code ist dies repariert indem einfach der Konstruktur nicht inline ist.
Die Compilierung und der Donwload auf dem PiPico dauert ewig
Bei mir war es nur beim ersten Mal langsam, danach wird der beim letzten Mal kompilierte Bibliothekscode wiederverwendet. Dann sind es nur noch ein paar Sekunden, die sich die Arduino-IDE selbst genehmigt, während der Compiler nur einen Bruchteil dieser Zeit ausmacht. Bei einer vernünftigen™ IDE sieht das natürlich besser aus.
Vielen Dank für das Programm. Es kompiliert in der Arduino-IDE fehlerfrei.
Ich werde die nächsten Tage mal versuchen, das Ganze genauer zu verstehen und etwas anzupassen.
Vielen Dank für das Programm. Es kompiliert in der Arduino-IDE
fehlerfrei.
Super!
In der gezeigten Version müssen wie gesagt alle Elemente vom selben Typ sein, aber man kann per std::variant jeweils eine von mehreren Klassen auswählen. Optional kannst du dann:
Du kannst die gemeinsame Basis trotzdem implementieren und
dann noch pro Element einen Pointer darauf behalten um die Funktionen
ohne std::visit aufzurufen, das ist dann aber etwas doppelt gemoppelt...
Eine abgewandelte Möglichkeit habe ich mal umgesetzt, und zwar eine Klasse sehr ähnlich zu std::variant, die immer genau eine Instanz aus einer Liste von N Klassen enthält, welche aber eine gemeinsame Basisklasse haben müssen. Meine Klasse speichert einen Pointer auf die Basis der jeweils erzeugten Instanz, welchen man dann abfragen kann um virtuelle Funktionen aufzurufen, wobei dann aber std::visit nicht mehr unterstützt wird. Das ist also Polymorphie bei statisch allokiertem Speicher. Letztendlich ist es eine union+Pointer mit schönerer Syntax drumherum.
Das Ganze hatte ich benutzt um das State Pattern zu implementieren, aber auf deinen Use Case würde es auch gut passen. Der Code ist hier:
Das müsste man auch wieder recht gut auf AVR-GCC anpassen können indem man die fehlenden Standard-Bibliotheks-Komponenten manuell hinzufügt. Der Code ist mittlerweile etwas alt, vermutlich kann man da bei aktuellen Compilern schon etwas aufpolieren/kürzen.
Zum Testen wegen Speicherleck. Man kann doch die Adresse der Instanz ermitteln. Dann kann man ein paar Speicherzellen sequentiell lesen und mit vorher/nachher den Inhalt vergleichen. Ob das Aussagekräftig ist weiß ich nicht. Denn überschrieben wird der freigegebene Speicher erst wenn Bedarf besteht.
Zudem, wie soll jemand auf einen µC an den Speicher rankommen zum auslesen? Man kann doch nachträglich keine Software zur Spionage installieren wie das auf einem PC möglich wäre.
Diese Flexibilität beim programmieren sollte man nicht unterschätzen, besonders wenn man sich als Ingenieur mal vor Augen führt was so ein Arbeitstag kostet.
Man kann ja auch nur während der Initialisierung des Programms dynamisch allozieren und dann zur Laufzeit nichts mehr daran ändern. Dann weiß man zur Startzeit sicher dass der Speicher reicht. Das ist zum Beispiel eine Lösung für DSP-artige Software wo der gleiche Code auf vielen Plattformen inkl PC (Simulation) laufen soll. Oder wenn die Firmware verschiedene Aufgaben erfüllen soll und das beim Start irgendwie ausgewählt wird.
Ich habe auch schon gesehen dass manche einen Bereich statisch reservieren und dort dann ihre eigene dynamische Speicherverwaltung nachbauen. Aber da kann man auch in der Regel auch gleich den existierenden Heap von libc bzw. des OS nehmen und muss nicht zig Arbeitsstunden versenken.
Ich denke dass man die Nutzung des Heap nicht kategorisch ausschließen sollte, auch nicht auf kleinen Mikrocontrollern. Wenn man seine Tools kennt und sich auch in libc und den ganzen low-Level Routinen zurecht findet, dann kann man auch gute Entscheidungen treffen und muss nicht irgendwelche Weisheiten aus dem Internet unhinterfragt glauben.
besonders wenn man sich als Ingenieur mal vor Augen führt was so ein
Arbeitstag kostet.
Die Arbeitszeit, die das Debuggen kostet, weil man bei irgendeinem
malloc() das Prüfen auf NULL vergessen hat, ist auch nicht 0...
Das streite ich nicht ab, gebe aber zu bedenken, dass Leute die std::variant als Lösung auch nur in Betracht ziehen, auch dazu neigen, kein malloc() händisch zu verwenden. In Bare-Metal Software würde man eher std::__throw_bad_alloc() überschreiben und bekäme so implizit Bescheid wenn der Heap alle ist. Mit dem ld Linker und dem nützlichen flag --wrap könnte man sich auch einfach in malloc() einklinken. Gibt viele Möglichkeiten.
Dass die Größe/Anzahl der Objekte/whatever von Parametern abhängen kann
und diese Parameter erst während der Laufzeit bekannt sind.
Was machst du dann mit dem Speicher der nicht benutzt wird weil die
Parameter klein waren?
Möchten Sie darauf hinaus, dass man dann auch gleich statisch ein Kontextobjekt für alle Parametervarianten allozieren könnte, verpackt in std::variant wie Sie bereits weiter oben vorgeschlagen haben? Auf jeden Fall kann man das machen. Mir geht es darum, dass man auch Klassen wie std::vector ohne Aufwand im Code verwenden kann. Ob das nun im Speziellen auf dem AVR so sinnvoll ist, sei mal dahin gestellt. Es macht aber den Code deutlich einfacher. Ihre Lösung mit dem globalen Kontextobjekt (ich interpretiere das jetzt einfach mal so) ist auf jeden Fall eine Möglichkeit wenn der Code ausschließlich als Firmware in einem eingebetteten System laufen soll. Spart dann oft auch noch Codegröße. Aber nehmen wir mal an, es handelt sich um eine DSP Applikation und jetzt wollen Sie zwei Instanzen desselben Codes zu Testzwecken in einer PC-Applikation laufen lassen - z.B. Sender und Empfänger, dann kann man nicht einfach globale Variablen nehmen. Das ist selbstverständlich alles lösbar, erzeugt aber Entwicklungskosten. Und da kann es manchmal bei aller Bastelfreudigkeit einfacher sein, den Heap zu verwenden wie er ist. Letztendlich ist der Heap ja auch ein statischer Block Speicher mit bekannter Größe und vorimplementierter Speicherverwaltung. Man sieht die Auslastung aber erst zur Laufzeit - bestenfalls nach der Initialisierung.
In Bare-Metal Software würde man
eher std::__throw_bad_alloc() überschreiben
Hast du da mal ein Beispiel, z.B. für AVR und Cortex-M
(GCC-ARM-Embedded)?
Die Funktion std::__throw_bad_alloc() wird vom Operator new in libstdc++v3 (GCC) aufgerufen und ist Teil von ebendieser. Man kann sie überschreiben und zum Programm dazu linken. Aber wie gesagt, ein --wrap von malloc() ist noch besser weil man dann auch C code mit abdeckt.
Möchten Sie darauf hinaus, dass man dann auch gleich statisch ein
Kontextobjekt für alle Parametervarianten allozieren könnte, verpackt in
std::variant wie Sie bereits weiter oben vorgeschlagen haben?
Aber nehmen wir mal an, es handelt sich um eine DSP Applikation und
jetzt wollen Sie zwei Instanzen desselben Codes zu Testzwecken in einer
PC-Applikation laufen lassen - z.B. Sender und Empfänger, dann kann man
nicht einfach globale Variablen nehmen.
Auch dann funktioniert das wunderbar, man legt einfach mehr Objekte an. Statt 2x malloc hat man eben 2 Zeilen mit den globalen Objekten. Auf dem PC kann man natürlich für diesen Zweck auch "new" nutzen, da spielt es keine Rolle.
Die Objekte sind zwar im globalen Speicher (letztendlich .data / .rodata / .bss), aber man sollte sie nicht im klassischen C-Sinne wie globale Objekte benutzen, sondern nur möglichst wenigen Stellen direkt darauf zugreifen. Typischerweise 1-2x in der main(), und ggf. noch in Interrupts. Das lässt sich z.B. erreichen inden man die Objekte als "static" anlegt in der main.cpp, welche ausschließlich die main() und die ISRs enthält. Alle anderen Zugriffe erfolgen indirekt über Referenzen/Pointer. Somit kann man ganz problemlos Objekte hinzufügen/ändern, man muss nur die ganz wenigen Zugriffe erweitern. Das muss man bei der dynamischen Allokation per "new" auch.
Aber wie gesagt, ein --wrap
von malloc() ist noch besser weil man dann auch C code mit abdeckt.
Naja, wenn man im Fehlerfall von malloc() z.B. das Programm anhält, passiert das auch in Fällen, wo der Rückgabewert von malloc() korrekt geprüft wird. Man möchte ja die Stellen finden, wo das nicht passiert.
Am Meisten würde mich das für std::__throw_bad_alloc() interessieren...
Wenn du die libstdc++v3 (also klassische GCC-Toolchain) verwendest, sorge dafür dass folgende Funktion in einer Objektdatei mit zur Applikation gelinkt wird:
1
// cxxabi_custom.cpp
2
namespacestd{
3
void__throw_bad_alloc(){
4
// Mach irgendwas sinnvolles
5
asmvolatile("nop");
6
}
7
}
Stecke sie nicht in eine statische Bibliothek. Und dann schaust du mal mit dem Debugger ob du dort im Fehlerfall landest und wenn nicht, warum nicht. Die Funktion ist ursprünglich hier definiert:
Wenn du die libstdc++v3 (also klassische GCC-Toolchain) verwendest,
sorge dafür dass folgende Funktion in einer Objektdatei mit zur
Applikation gelinkt wird:
Mit -Wl,--gc-sections landet die Funktion bei mir so nicht bei mir im Assembly-Listing (wird wegoptimiert), funktioniert also nicht. Ohne -Wl,--gc-sections wird sie zwar natürlich nicht wegoptimiert, wird aber nie aufgerufen oder als Funktionszeiger genutzt. Mit dem GCC-ARM-Embedded für Cortex-M.
Wenn du die libstdc++v3 (also klassische GCC-Toolchain) verwendest,
Um das nur kurz im Kontext des Threads hier zu bewerten: hier geht's um einen AVR mit Arduino. Das ist zwar gcc und C++, Exceptions gibt's da aber keine.
Um das nur kurz im Kontext des Threads hier zu bewerten: hier geht's um
einen AVR mit Arduino. Das ist zwar gcc und C++, Exceptions gibt's da
aber keine.
Nicht ganz. Es geht um ein "Design Pattern" mit dem man eine Liste von Objectinstanzen anlegen kann und dann die gesamte Liste wieder löschen kann.
Die Zielarchitekturen sind dabei:
Atmega328
PiPico
PC
Das Design Pattern soll auf allen Architekturen funktionieren.
Wenn du die libstdc++v3 (also klassische GCC-Toolchain) verwendest,
Um das nur kurz im Kontext des Threads hier zu bewerten: hier geht's um
einen AVR mit Arduino. Das ist zwar gcc und C++, Exceptions gibt's da
aber keine.
Obiger Link erklärt auch, warum Niklas keinen Aufruf von std::__throw_bad_alloc() sieht. Nicht die Implementierung von operator new() ruft std::__throw_bad_alloc() auf sondern der Standardallokator std::new_allocator der auch von den std Container genutzt wird. Sobald man zum Beispiel std::vector benutzt, ist die Funktion dabei, bei einem einfachen new MeineKlasse{} aber nicht. Mea culpa.
Bleiben die Möglichkeiten, (1) die diversen Operator new mit einer eigenen Implementierung zu ersetzen oder (2) sich in __cxa_throw() einzuklinken bzw. zu ersetzen und auf einen bad_alloc() Parameter zu prüfen oder (3) sich mit --wrap gleich in malloc() einzuklinken welches am Ende immer aufgerufen wird und dort zu prüfen ob die Allokation erfolgreich war.
Hehe, volle Zustimmung. Deine Lösung mit std::variant oder einem
dummen union ist in Zuverlässigkeit und Vorhersagbarkeit natürlich allem anderen überlegen. Aber es wird der Tag kommen wo du ein Stück Software vorgesetzt bekommst und irgend einen dummen Fehler bei der dynamischen Speicherallokation finden sollst.
Aber es wird der Tag kommen wo du ein Stück Software vorgesetzt bekommst
und irgend einen dummen Fehler bei der dynamischen Speicherallokation
finden sollst.
Bei solchen Fällen würde man wahrscheinlich eh nicht den Code modifizieren sondern Breakpoints in malloc() setzen. Solche Fehler sind ja gern mal "Heisenbugs"...
Wenn du die libstdc++v3 (also klassische GCC-Toolchain) verwendest,
Um das nur kurz im Kontext des Threads hier zu bewerten: hier geht's um
einen AVR mit Arduino. Das ist zwar gcc und C++, Exceptions gibt's da
aber keine.
Oliver
Halte ich für teilweise richtig und wir kompilieren hier im Thread auch
mit -fno-exceptions.
Vielleicht noch etwas deutlicher: AVR Arduino ist zwar C++, aber ohne jegliche libstdc++. Da gibts nur die Arduino-lib, die etwas C++-Flair verbreitet, ohne exceptions, handler, und sonstiges.
Aber die Exception handler werden trotzdem
aufgerufen, werfen dann nur keine Exceptions sondern rufen
std::terminate() und letztendlich abort() auf.
Es gibt für AVRs überhaupt gar keine keine libstdc++, und keine Exception handler, exceptions, oder irgend etwas in der Art. Damit ist die ganze Diskussion hier müßig.
Es tut mir sehr leid, dass ich diese Funktionen Exception Handler genannt habe. Das war wirklich sehr missverständlich von mir ausgedrückt. In der GNU libstdc++ sollen diese Funktionen die eigentlichen Exceptions werfen, sofern Exceptions aktiviert sind. Ansonsten terminieren sie das Programm wie im Link gezeigt.
Ist eine partielle Implementierung von ein paar wenigen Features. Gut gemacht, und sinnvoll, aber keine libstdc++, und natürlich ohne jegliche Exceptions.
xception Handler übersetzt der AVR-GCC sehr wohl (wenn man -fexceptions
angibt):
void test () {
try {
foo ();
} catch (int err) {
puts ("Error");
}
}
Ja, und weils nutzlos ist, optimiert der das dann auch gleich alles spurlos weg.
Wollte mans wirklich nutzen, und packt in foo() ein throw dazu, stolpert der linker über die fehlenden lib-Funktionen.
Es ist, wie es ist: C++ auf dem AVR ist ein stark eingeschränkter Sonderfall, ohne stdlibc++ und Exceptions.
Es ist, wie es ist: C++ auf dem AVR ist ein stark eingeschränkter
Sonderfall, ohne stdlibc++ und Exceptions.
Ja, selbst auf Cortex-M ist es zwar möglich aber wenig sinnvoll. Zumindest bei der GCC-ARM-Embedded Toolchain hat es einen großen Overhead bei sehr wenig Nutzen; die Art von Fehler und Fehlerbehandlung, für die Exceptions konzipiert sind, tritt bei Embedded Systemen einfach kaum auf. Daher schrieb ich ja schon: Speicher statisch allozieren.
Höchstens vielleicht wenn man sowas wie eine SPS implementiert, wo man aufgrund einer Konfigurationsdatei dynamisch ein Modell aufbaut; dann nutzt man den Speicher entweder für 3 PID Regler oder ein Moving Average oder oder... Für die kurze Phase des Modell-Aufbaus können Exceptions den Code vermutlich vereinfachen, aber ob sich das lohnt?
Interessant wäre, wie viel Overhead C++ erzeugt. Es gibt ja Leute die behaupten, man könne C++ so programmieren, dass kein Overhead entsteht. Bei meine Experimenten schien es aber so zu sein, dass selbst nur das Einbinden der Objektblibliotheken ohne eine einzige erzeugte Instanz schon RAM verbraucht hat. Die Objektinstanzen selbst fressen dann mindestens 16 Bytes selbst wenn sie keine lokale Variablen haben.
Beim GCC-ARM-Embedded + STM32 Setup: 52 Bytes Flash, für die Schleife im Reset_Handler um die Konstruktoren globaler Objekte aufzurufen. Die wird nicht wegoptimiert wenn keine globalen Objekte vorhanden sind (wäre interessant zu analysieren ob das mit dem GNU LD ginge).
Es gibt ja Leute die
behaupten, man könne C++ so programmieren, dass kein Overhead entsteht.
Da gehöre ich zu, bis auf diese 52 Bytes. Es kommt natürlich stark darauf an, welche Sprachfeatures man verwendet. Exceptions haben wie gesagt auf Mikrocontrollern einen großen Overhead gegenüber traditioneller Fehlerbehandlung. Bei PC/Server-Programmierung sieht das wieder ganz anders aus, da kann Exception-basierter Code sogar schneller sein. Der Unwinding-Code existiert dann genau 1x auf dem System, in der libstdc++.
Bei meine Experimenten schien es aber so zu sein, dass selbst nur das
Einbinden der Objektblibliotheken ohne eine einzige erzeugte Instanz
schon RAM verbraucht hat.
Ich denke mal du meinst mit Objektbibliothek eine gewöhnliche statische Bibliothek (.a / .o) in welcher C++ Klassen implementiert sind (d.h. der Code der Memberfunktionen).
Dann solltest du mal mit -ffunction-sections -fdata-sections kompilieren und mit -Wl,--gc-sections linken. Dann werden nicht genutzte globale Objekte wegoptimiert, außer der Autor hat "attribute((used))" dran geschrieben.
Das ist natürlich 100% unabhängig von C vs. C++, auch statische C-Libraries können globale Variablen beinhalten die dann beim Einbinden natürlich direkt RAM belegen. Eigentlich sind gerade C-Bibliotheken gern so implementiert.
Die Objektinstanzen selbst fressen dann
mindestens 16 Bytes selbst wenn sie keine lokale Variablen haben.
Lokale Variablen gibt es nur innerhalb von Funktionen, nicht in Objekten. C++ Objekte sind, genau wie C-Variablen, mindestens 1 Byte groß. Das muss so, damit Zeiger auf verschiedene Instanzen unterschiedlich sind, genau wie in C. Außer man verwendet alignas(), um ein Alignment von z.B. 16 Byte zu erzwingen, genau wie in C. Mit "no_unique_address" kann man in C++ dafür sorgen, dass mehrere leere Member-Variablen innerhalb einer Klasse die Größe 0 bekommen.
Dank templates und inlining kann C++ Code durchaus schneller als äquivalenter C-Code sein, bzw. ein gleich schneller C-Code wäre extrem umständlich zu implementieren.
Aber die Exception handler werden trotzdem
aufgerufen, werfen dann nur keine Exceptions sondern rufen
std::terminate() und letztendlich abort() auf.
Es gibt für AVRs überhaupt gar keine keine libstdc++, und keine
Exception handler, exceptions, oder irgend etwas in der Art. Damit ist
die ganze Diskussion hier müßig.