cpp memory leaks vermeiden

#7834795
Lesenswert?

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
class Counter {
4
private:
5
    int count;
6

7
public:
8
    Counter() : count(0) {}  // Constructor initializes count to 0
9

10
    void exec() {
11
        count++;
12
    }
13

14
    int getCount() const {
15
        return count;
16
    }
17
};
18

19
class Counter_IF {
20
private:
21
    Counter counterInstance;  // Instance of Counter
22

23
public:
24
    void exec() {
25
        counterInstance.exec();  // Call exec on the Counter instance
26
    }
27

28
    int getCount() const {
29
        return counterInstance.getCount();  // Retrieve count from the Counter instance
30
    }
31
};
32

33
int main() {
34
    Counter_IF counters[5];  // Array of 5 Counter_IF instances
35

36
    // Call exec on some of the counters
37
    counters[0].exec();
38
    counters[1].exec();
39
    counters[1].exec();
40
    counters[2].exec();
41
    counters[2].exec();
42
    counters[2].exec();
43

44
    // Display the counts of all counters
45
    for (int i = 0; i < 5; i++) {
46
        std::cout << "Counter " << i << " count: " << counters[i].getCount() << std::endl;
47
    }
48

49
    return 0;
50
}
#7834829
Lesenswert?

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?

1
#define COUNTERLIST_LEN 10
2
int main() {
3

4
    Counter_IF *Liste[COUNTERLIST_LEN];
5

6
    int idx=0;
7

8
    // Zaeher anlegen
9
    int numCounters=5;
10
    for(idx=0;idx<numCounters;idx++) Liste[idx] = new Counter_IF();
11

12
    // Zaehler laufen lassen
13
    for(idx=0;idx<numCounters;idx++) Liste[idx]->exec();
14

15
    // Zaeher new anlegen
16
    numCounters=8;
17
    for(idx=0;idx<numCounters;idx++) Liste[idx] = new Counter_IF();
18

19
    // Zaehler laufen lassen
20
    for(idx=0;idx<numCounters;idx++) Liste[idx]->exec();
21

22
    // Anzeigen
23
    for(idx=0;idx<numCounters;idx++)
24
    {
25
        std::cout << "Counter " << idx << " count: " << Liste[idx]->getCount() << std::endl;
26
    }
27

28
    return 0;
29
}
#7834846
Lesenswert?

Um das genauer zu untersuchen, brauch man wohl erst einmal einen "Speicherverbrauchsmesser".
Hier mein Versuch mit einem Arduino Nano.

Seltsamerweise scheint das Anlegen des Buffers keinen Speicher zu brauchen. Es sollten ja 10 Byte mehr sein ..

Die Frage ist: sind die Funktionen für die "Verbrauchsmessung" geeignet?

Free RAM: 1850
heapStartMemAddress: 1577
lastHeapMemAddress: 0
Free RAM: 1850

1
#include <stdlib.h>
2

3
/*
4
 Memory allocation: __brkval points to the last memory address used by the heap. 
5
 When dynamic memory is allocated (e.g., using malloc()),
6
  __brkval is adjusted to reflect the new heap boundary.
7
*/
8
int lastHeapMemAddress()
9
{
10
  extern int *__brkval;
11
  return (int) __brkval;
12
}
13
int heapStartMemAddress()
14
{
15
  extern int *__heap_start;
16
  return (int) __heap_start;
17
}
18

19
int freeRam() {
20
  
21
/*
22
    It checks if __brkval is 0.
23
    If __brkval is 0 (meaning no heap allocations have been made yet), it calculates the free memory from &v to &__heap_start.
24
    If __brkval is not 0, it calculates the free memory from &v to __brkval.
25
    Finally, it returns the calculated free memory.
26
*/
27
  extern int __heap_start, *__brkval;
28
  int v;
29
  int free_memory;
30

31
  if (__brkval == 0) {
32
    free_memory = (int) &v - (int) &__heap_start;
33
  } else {
34
    free_memory = (int) &v - (int) __brkval;
35
  }
36

37
  return free_memory;
38
}
39

40

41
void setup() {
42
  Serial.begin(115200);
43
  Serial.println();
44
  Serial.print(F("Free RAM: "));  Serial.println(freeRam());
45
  Serial.print(F("heapStartMemAddress: "));  Serial.println(heapStartMemAddress());
46
  
47
  uint8_t buffer[3]={0};
48
  
49
  Serial.print(F("lastHeapMemAddress: "));  Serial.println(lastHeapMemAddress());
50
  Serial.print(F("Free RAM: "));  Serial.println(freeRam());
51
  for(int n=0;n<10;n++)Serial.println(buffer[n],HEX);
52

53
}
54

55
void loop() {
56
  // Your code here
57
}
Persönliche Seite #7834857
Lesenswert?

Christoph M. schrieb:

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.

Christoph M. schrieb:

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!

#7834865
Lesenswert?

Christoph M. schrieb:

Mein Beispiel war etwas irreführend

Christoph M. schrieb:

new

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.

#7834873
Lesenswert?

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.

#7834898
Lesenswert?

Wie man einen Speicherleck vermeidet?

  • Requirements schreiben + Review machen
  • Architektur aufmalen + Review machen
  • Design von Modulen machen + Review machen
  • 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.

#7834945
Lesenswert?

Niklas G. (erlkoenig)

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:

  1. Ein Mikrocontroller soll über die serielle Schnittstelle Kommandos empfangen können.
  2. Bei jedem Kommando "counter" soll die Instanz eines Zähler erzeugt werden. Verweise auf die Instanzen sollen in ein Array eingetragen werden.
  3. Beim Empfang des Kommandos "run" soll die Exec-Function aller im Array befindlichen Objekte aufgerufen werden.
  4. Es sollen so viele Objekte erzeugt werden können, wie in den Speicher passen
  5. Empfängt der Mikrocontroller den Befehl 'delete' über die serielle Schnittstelle, sollen alle Objekte gelöscht und er Speicher wieder frei gegeben werden.
  6. Schritt 2 soll wiederholt werden können.
  7. Es soll möglich sein, das Programm zu erweitern für neue Kommados z.B. "generator" (Signalgenerator)

Review machen:

#7834984
Lesenswert?

Oliver S. (oliverso) 24.02.2025 13:20

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.

#7835009
Lesenswert?

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.

1
#define LEDARRAYT_LEN 10
2

3
Flasher *Liste[LEDARRAYT_LEN];
4

5

6
void setup()
7
{
8
  Serial.begin(115200);
9
  Idx = 0;
10
  Liste[Idx++] = new Flasher(12, 100, 400);
11
  Liste[Idx++] = new Flasher(13, 350, 350);
12
}
13

14
void loop()
15
{
16
  // run the objects is no serial char availabe
17
  while (!Serial.available())
18
    for (int n = 0; n < Idx; n++) Liste[n]->exec();
19

20
  // check the commands
21
  char c = Serial.read();
22
  
23
  if (c == 'd')
24
  {
25
    Serial.println("delete");
26
    Idx = 0;
27
    for (int n = 0; n < Idx; n++) delete(Liste[n]);
28
  }
29
  
30
  if (c == 'n')
31
  {
32
    Serial.println("create new list");
33
    Idx = 0;
34
    Liste[Idx++] = new Flasher(12, 100, 400);
35
    Liste[Idx++] = new Flasher(13, 350, 350);
36
  }
37
  
38
  if (c == 'h')
39
  {
40
    Serial.println("== help ==");
41
    Serial.println("d: delete");
42
    Serial.println("n: new list");
43
  }
44

45
}
Angehängte Dateien:
#7835025
Lesenswert?

Michael B. (laberkopp) 24.02.2025 16:55

Christoph M. schrieb:

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.

Aber leider der Speicher wohl nicht frei gegeben.

Persönliche Seite #7835078
Lesenswert?

Christoph M. schrieb:

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

Christoph M. schrieb:

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.

Christoph M. schrieb:

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:

Christoph M. schrieb:

Fixe Instanzen geht leider nicht.

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.

Angehängte Dateien:
#7835111
Lesenswert?

Niklas G. schrieb:

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

Bei dir. Offensichtlich.

Persönliche Seite #7835114
Lesenswert?

Michael B. schrieb:

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.

Michael B. schrieb:

Bei dir. Offensichtlich.

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.

#7835163
Lesenswert?

Michael B. schrieb:

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.

#7835168
Lesenswert?

Richard W. schrieb:

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.

#7835272
Lesenswert?

Niklas G. (erlkoenig)

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.

Siehe die Anforderungen hier:
Beitrag "Re: cpp memory leaks vermeiden"

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.

Persönliche Seite #7835299
Lesenswert?

Christoph M. schrieb:

Siehe die Anforderungen hier:

Naja da steht nur dass es dynamische Speicherverwaltung sein "soll", aber kein Grund dafür.

Christoph M. schrieb:

Der Atmega hat den Vorteil, dass man ultraschnell kompilieren und testen kann.

Bei den STM32 und mit einem schnellen Debugger/Flasher wie J-Link ist es auch extrem schnell. Und debuggen kann man damit auch fantastisch.

Christoph M. schrieb:

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

Christoph M. schrieb:

Das gesamte Array und die Klassen müssen in einem Rutsch gelöscht werden können,

Meine InplaceVector Klasse hat dafür eine "clear" Funktion.

Christoph M. schrieb:

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.

#7835313
Lesenswert?

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.

#7835332
Lesenswert?

Christoph M. schrieb:

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.

Angehängte Dateien:
Persönliche Seite #7835357
Lesenswert?

Roger S. schrieb:

Der alloziert nur speicher wenn er waechst

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.

#7835373
Lesenswert?

Niklas G. schrieb:

Roger S. schrieb:

Der alloziert nur speicher wenn er waechst

Braucht aber trotzdem malloc/new und zieht damit eine Menge Code aus der Standard Bibliothek rein.

und? was ist das Problem hier?

Was macht man mit dem freigewordenen Speicher wenn man den std::vector leert? Nichts?

wenn man den vector auf der maximalen Groesse belaesst, dann hat man keine allokationen mehr, kann in einem embedded system von Vorteil sein.

Dann kann man auch einen in-place-vector nehmen.

kann man, wenn man den dann hat. Out-of-the-box support sieht schlecht aus.

Ganz nebenbei müssen die Elemente dafür auch nicht kopierbar/move-bar sein.

nope.

Persönliche Seite #7835386
Lesenswert?

Roger S. schrieb:

und? was ist das Problem hier?

Ist einfach unnötige Verschwendung von Flash, Rechenzeit und RAM.

Roger S. schrieb:

wenn man den vector auf der maximalen Groesse belaesst, dann hat man keine allokationen mehr, kann in einem embedded system von Vorteil sein

Was ist der Vorteil gegenüber der Lösung, den Speicher einfach direkt vom Linker allokieren zu lassen?

Roger S. schrieb:

kann man, wenn man den dann hat. Out-of-the-box support sieht schlecht aus.

Aber wie gezeigt kann man sich den sehr leicht selbst implementieren und hat dann minimalen Aufwand in RAM, Flash, Rechenzeit.

Roger S. schrieb:

nope.

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.

#7835404
Lesenswert?

Niklas G. schrieb:

Roger S. schrieb:

und? was ist das Problem hier?

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.

Weiss nicht was du damit zum Ausdruck bringen willst, aber: https://godbolt.org/z/YzGxvc9PM

Persönliche Seite #7835413
Lesenswert?

Roger S. schrieb:

gibt es Geld zurueck fuer nicht genutzen Flash?

Nein, aber man kann mehr Funktionalität rein packen.

Roger S. schrieb:

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

Roger S. schrieb:

Standard Library Komponente.

Ist beim AVR-GCC eh nicht verfügbar.

Roger S. schrieb:

Ansonsten identisch.

Bis auf Flash,Ram,Rechenzeit halt, aber wen interessieren die schon, gerade auf einem AVR.

Roger S. schrieb:

Leicht? Fuer einen Anfaenger verstaendlich?

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.

Roger S. schrieb:

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.

#7835691
Lesenswert?

Vielen Dank für eure Beiträge.

Ich habe mal als einfacher Test den Code von Niclas https://www.mikrocontroller.net/attachment/highlight/662012
mit den verschiedenen Systemen compiliert, auf dem das laufen soll.

PC: geht fehlerfrei.

Arduino IDE, Board Arduino Nano:

1
Detecting libraries used...
2
/home/christoph/tools/Arduino/arduino-1.8.6/hardware/tools/avr/bin/avr-g++ -c -g -Os -w -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -flto -w -x c++ -E -CC -mmcu=atmega328p -DF_CPU=16000000L -DARDUINO=10806 -DARDUINO_AVR_NANO -DARDUINO_ARCH_AVR -I/home/christoph/tools/Arduino/arduino-1.8.6/hardware/arduino/avr/cores/arduino -I/home/christoph/tools/Arduino/arduino-1.8.6/hardware/arduino/avr/variants/eightanaloginputs /tmp/arduino_build_60317/sketch/TEST_objectDelete.ino.cpp -o /dev/null
3
TEST_objectDelete:1:19: error: cstddef: No such file or directory
4
compilation terminated.
5
exit status 1
6
cstddef: No such file or directory

Arduino IDE, Board PiPico:

1
Sketch wird kompiliert...
2
....
3
TEST_objectDelete:11:17: error: 'constexpr InplaceVector<T, N>::Container::Container()' cannot be overloaded with 'constexpr InplaceVector<T, N>::Container::Container()'
4
   11 |       constexpr Container () {}
5
      |                 ^~~~~~~~~
6
/home/christoph/Entwicklung/250204_GraphProg2025/ArduinoVM/TEST_objectDelete/TEST_objectDelete.ino:11:11: note: previous declaration 'constexpr InplaceVector<T, N>::Container::Container()'
7
   11 |       constexpr Container () {}
8
      |           ^~~~~~~~~
9
/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
10
exit status 1
11
'constexpr InplaceVector<T,
Persönliche Seite #7835755
Lesenswert?

Christoph M. schrieb:

Arduino IDE, Board Arduino Nano:

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.

Christoph M. schrieb:

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.

Christoph M. schrieb:

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.

Angehängte Dateien:
Persönliche Seite #7836140
Lesenswert?

Christoph M. schrieb:

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:

Niklas G. schrieb:

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:

https://github.com/Erlkoenig90/OverlayFSM/blob/main/inc/OverlayFSM/overlay_fsm.hh

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.

Damit könntest du dann z.B. sowas machen:

1
InplaceVector<OverlayFSM<Base, Derived1, Derived2, Derived3>> counters;
2

3
int main () {
4
  // Derived1-Instanz hinzufügen
5
  counters.emplace_back (static_cast<Derived1*> (nullptr), ... /* Konstruktor-Parameter für Derived1 */ );
6

7
  // Derived2-Instanz hinzufügen
8
  counters.emplace_back (static_cast<Derived2*> (nullptr), ... /* Konstruktor-Parameter für Derived2 */ );
9
  
10
  // 2. Instanz auf Derived1 ändern
11
  counters [1].go <Derived1> (... /* Konstruktor-Parameter für Derived1 */);
12

13
  // virtuelle Funktionen aufrufen
14
  for (Base& obj : counters)
15
    obj.current ().exec ();
16
  
17
  // Alle löschen
18
  counters.clear ();
19
}
#7841389
Lesenswert?

Hallo,

wenn es um eine avr LibStdCpp geht, die hier kann man verwenden. https://github.com/modm-io/avr-libstdcpp

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.

#7842527
Lesenswert?

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.

Persönliche Seite #7842529
Lesenswert?

Richard W. schrieb:

Oder wenn die Firmware verschiedene Aufgaben erfüllen soll und das beim Start irgendwie ausgewählt wird.

Da kann man auch einfach ein std::variant der diversen Hauptklassen für die einzelnen Use Cases nehmen.

Richard W. schrieb:

Man kann ja auch nur während der Initialisierung des Programms dynamisch allozieren

Und wo besteht dann der Vorteil davon, gegenüber dem simplen Anlegen als globale (static) Variablen?

Richard W. schrieb:

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

#7842547
Lesenswert?

Niklas G. schrieb:

Richard W. schrieb:

Oder wenn die Firmware verschiedene Aufgaben erfüllen soll und das beim Start irgendwie ausgewählt wird.

Da kann man auch einfach ein std::variant der diversen Hauptklassen für die einzelnen Use Cases nehmen.

Möglich, std::variant ist auch schnell mal ausprobiert und dann sieht man ja ob es passt.

Richard W. schrieb:

Man kann ja auch nur während der Initialisierung des Programms dynamisch allozieren

Und wo besteht dann der Vorteil davon, gegenüber dem simplen Anlegen als globale (static) Variablen?

Dass die Größe/Anzahl der Objekte/whatever von Parametern abhängen kann und diese Parameter erst während der Laufzeit bekannt sind.

Richard W. schrieb:

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.

Persönliche Seite #7842785
Lesenswert?

Richard W. schrieb:

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?

Richard W. schrieb:

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)?

#7843239
Lesenswert?

Niklas G. schrieb:

Richard W. schrieb:

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.

Richard W. schrieb:

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.

Persönliche Seite #7843252
Lesenswert?

Richard W. schrieb:

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?

Ja. Hat bisher immer perfekt funktioniert.

Richard W. schrieb:

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.

Richard W. schrieb:

Man sieht die Auslastung aber erst zur Laufzeit

Das ist für mich schon ein Knackpunkt, hat schon Vorteile wenn der Linker dies direkt ausgeben kann.

Richard W. schrieb:

Man kann sie überschreiben und zum Programm dazu linken.

Also muss man die Standardbibliothek neu kompilieren?

Richard W. schrieb:

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.

#7843345
Lesenswert?

Niklas G. schrieb:

Richard W. schrieb:

Nein. Symbole die beim linken in den Object Dateien definiert sind, haben immer Vorrang gegenüber Symbolen aus statischen Bibliotheken.

So pauschal ist das falsch...

Okay, kannst du dann mal ein Beispiel zeigen wie man das im User Code überschreibt?

Als Einstieg: https://www.gnu.org/software/libc/manual/html_node/Replacing-malloc.html

Oliver

#7843458
Lesenswert?

Niklas G. schrieb:

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
namespace std {
3
    void __throw_bad_alloc() {
4
        // Mach irgendwas sinnvolles
5
        asm volatile ("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:

https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/src/c%2B%2B11/functexcept.cc

Persönliche Seite #7843462
Lesenswert?

Richard W. schrieb:

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.

#7843472
Lesenswert?

Oliver S. (oliverso) 11.03.2025 16:21

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.

#7843585
Lesenswert?

Oliver S. schrieb:

Richard W. schrieb:

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. Aber die Exception handler werden trotzdem aufgerufen, werfen dann nur keine Exceptions sondern rufen std::terminate() und letztendlich abort() auf. Sieht man zum Beispiel hier: https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=libstdc%2B%2B-v3/libsupc%2B%2B/new_op.cc;h=a45408b349ab3eabc905e1575586ab59af7de3f8;hb=HEAD

Das Macro wird hier definiert, abhängig davon ob Exceptions aktiviert sind: https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=libstdc%2B%2B-v3/include/bits/c%2B%2Bconfig;h=676f5eecbbb6a715800a99aa4976fd8e51826dde;hb=HEAD#l260

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.

Persönliche Seite #7843636
Lesenswert?

Richard W. schrieb:

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

#7844648
Lesenswert?

Richard W. schrieb:

Oliver S. schrieb:

Richard W. schrieb:

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.

Oliver

#7844661
Lesenswert?

Ich meine die

Richard W. schrieb:

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.

Oliver

Persönliche Seite #7844679
Lesenswert?

Oliver S. schrieb:

Es gibt für AVRs überhaupt gar keine keine libstdc++, und keine Exception handler, exceptions, oder irgend etwas in der Art

Exception Handler übersetzt der AVR-GCC sehr wohl (wenn man -fexceptions angibt):

1
void test () {
2
    try {
3
        foo ();
4
    } catch (int err) {
5
        puts ("Error");
6
    }
7
}

Man kann halt nur nichts damit machen weil es kein Unwinding gibt. Theoretisch könnte man das implementieren...

#7844685
Lesenswert?

Oliver S. schrieb:

Es gibt für AVRs überhaupt gar keine keine libstdc++,

https://github.com/modm-io/avr-libstdcpp

Du meintest vielleicht, dass die GCC Toolchain das nicht mitbringt.

und keine Exception handler

https://github.com/modm-io/avr-libstdcpp/blob/master/src/functexcept.cc

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.

#7844695
Lesenswert?

Richard W. schrieb:

https://github.com/modm-io/avr-libstdcpp

Ist eine partielle Implementierung von ein paar wenigen Features. Gut gemacht, und sinnvoll, aber keine libstdc++, und natürlich ohne jegliche Exceptions.

Niklas G. schrieb:

Man kann halt nur nichts damit machen weil es kein Unwinding gibt. Theoretisch könnte man das implementieren...

Vermutlich könnte man das sogar praktisch, hat nur noch niemand gemacht. Obs auf einem AVR sinnvoll ist, ist allerdings fraglich.

Oliver

#7845091
Lesenswert?

Niklas G. schrieb:

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.

Oliver

Persönliche Seite #7845096
Lesenswert?

Oliver S. schrieb:

Ja, und weils nutzlos ist, optimiert der das dann auch gleich alles spurlos weg

Bei mir nicht, wenn foo() nicht definiert, nur deklariert ist.

Oliver S. schrieb:

Wollte mans wirklich nutzen, und packt in foo() ein throw dazu, stolpert der linker über die fehlenden lib-Funktionen.

So ist es... aber der Compiler kann mit den Exception Handlern grundsätzlich umgehen.

Oliver S. schrieb:

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?

#7845241
Lesenswert?

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.

Persönliche Seite #7845245
Lesenswert?

Christoph M. schrieb:

Interessant wäre, wie viel Overhead C++ erzeugt.

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

Christoph M. schrieb:

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

Christoph M. schrieb:

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.

Christoph M. schrieb:

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.

#7845370
Lesenswert?

Oliver S. schrieb:

Ich meine die

Richard W. schrieb:

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.

Genau genommen ist die libsupc++ dafür zuständig.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren