Stack & Heap --> Praktische Beispiele

Hi,

sorry, dass ich noch ein Thema parallel aufmache, aber:
Das Thema Stack & Heap hab ich noch nicht so ganz verstanden.
Ich hab mir mal den Link hier durchgelesen: https://lerneprogrammieren.com/stack-und-heap/
Die Unterschiede hab ich theoretisch verstanden. Der Heap kann mir quasi überlaufen (fragmentieren) wohingegen der Stack wieder "vergisst", d.h. oben was reinkommt und unten dafür wieder was raus.

Aber kann mir mal jemand ein paar praktische / typische Code-Beispiele nennen, wann etwas im Stack und wann im Heap gespeichert wird?

Hier mal ein Beispielszenario:

int wert_fünf=12;
int wert_sechs;
const int wert_sieben=12;
static const int wert_acht=10;
static const char wert_neun[] PROGMEM = R"=====(
  Hier steht was
)=====";


void loop() {
   int wert_vier=10;
   wert_sechs = 5;
   test(); 
}

function test() {
  int wert_eins = 5; 
  int wert_zwo = 10;
  int wert_drei = (wert_eins + wert_zwo); 
}

Gehe ich Richtig in der Annahme, dass
wert_fünf bis wert_neun, im Heap gespeichert sind und die restlichen im Stack?

Oder habt ihr andere CODE-Beispiele, die den Unterschied der beiden Speicherarten zeigen?

Ich will und muss das jetzt verstehen :wink:
Bitte wenns geht praktische Beispiele :smiley_cat:

Nein... wert_neun befindet sich im Flash-Speicher (PROGMEM), es wird dort zur Kompilierzeit initialisiert.

lokale Variablen gehen auf den Stack

Um etwas in den Heap zu bekommen, müssten Sie zum Beispiel malloc() verwenden

Nein. Deine globalen Variablen liegen in einem extra Teil der RAM,
oder nur im Flash für PROGMEM.

Heap Speicher wird per new/malloc angelegt.

Hast du einen klassischen Anwendungsfall, wann es Sinn macht (oder sogar nur möglich ist) den Heap zu verwenden? Ich glaub den hab ich dan noch nie (zumindest bewusst) befüllt.
Evtl. nutzt das dann irgendeine Library (wahrscheinlich z.B: ArduinoJSON wenn ich DynamicJSONDoc verwende) und ich bekomm es nicht mit.

magst mal die Einführung von Adafruit lesen?

You know you have a memory problem when... | Memories of an Arduino | Adafruit Learning System

Ein Beispiel für sinnvolle Heap Nutzung ist z.B. eine Library wie FastLed.

Um nicht immer einen maximal großen Puffer für die LEDs anlegen zu müssen,
wie das bei statischer Allocation der Fall wäre.
wird da der Speicher für den Puffer vom Heap genommen.

Türlich, les ich mir mal durch.
Mir hilft aber halt oft ein praktisches Beispiel / ein klassischer Anwendungsfall für das erste Verständnis.

Sehe ich es richtig, dass auch ArduinoJSON mit dem Heap arbeitet, wenn man sich für das DynamicJsonDocument entscheidet? Hier könnte ich ja dann einen "maximal großen Puffer" definieren, weil die Library selbst dann trotzdem dafür sorgt, dass dieser Puffer wieder leer geräumt wird,oder?

vieleicht mag das wer kontrolllesen:

int wert_fünf=12;                                            // static
int wert_sechs;                                              // static
const int wert_sieben=12;                                    // static
static const int wert_acht=10;                               // static
static const char wert_neun[] PROGMEM = R"=====(
  Hier steht was
)=====";


void loop() {
   int wert_vier=10;                                         // stack
   wert_sechs = 5;                                           // war vorher schon static - ist immer noch
   test(); 
}

function test() {
  int wert_eins = 5;                                         // stack
  int wert_zwo = 10;                                         // stack
  int wert_drei = (wert_eins + wert_zwo);                    // stack
  static wert_zweiundzwanzing = 22;                          // static
}

Klingt logisch.

Bei original Arduinos ist das aufgrund des Mini-Speichers alles eher akademisch,
wenn auch von String z.B. heftig missbraucht.

Wenn du eine Funktion aufrufst, verbraucht diese Speicher, z.B. für die lokal verwendeten Variablen. All das wird auf den Stack (bildlich einem Stapel) abgelegt.

Deine test Funktion braucht 3 Integer, also liegen 3 Integer auf dem Stack (und noch ein bisschen mehr).
Wenn test fertig ist, braucht das Programm wert_eins, wert_zwo und wert_drei aus diesem test Aufruf nicht mehr. Also kann das Programm das obere vom Stapel wieder aufräumen.

Wenn du eine Funktion aus einer Funktion aus einer Funktion aufgerufen werden, Beispiel loop -> test1 -> test2, dann liegen die lokalen Variablen von loop, test1 und test2 auf dem Stack. Allerdings arbeitet das Programm gerade in test2 und kann entsprechend nur die Variablen von test2 lesen. Variablen aus loop und test1 können nicht gelesen werden.
Wenn test2 fertig ist, liegen nur noch loop und test1 auf dem Stack.

Willst du allerdings von überall auf Daten zugreifen, müssen diese entweder im Flash sein, oder auf dem Heap. Um Werte aus dem Heap zu lesen, musst du aber auch wissen, wo diese liegen. Dafür gibt es Pointer.

I.d.R. benutzt du aber den Heap, um dynamisch große Daten zu speichern. Klassiker sind hier abstrakte Datentypen, wie verkettete Listen oder Bäume. Diese wachsen und schrumpfen mit der Laufzeit.

Bei der verketteten Liste hast du einen Pointer auf das erste Glied. Jedes Glied enthält dabei Daten und einen Pointer auf das nächste Glied. Die Kette kann "unendliche" lang sein und endet erst dann, wenn der Pointer auf null zeigt.

Beispiel: Du willst mehrere Eingaben speichern. Für jede neue Eingabe wird mit malloc ein neues Glied erzeugt und hinten an die Kette gehängt.
Sobald du ein Glied von der Kette verarbeitet hast, kannst du es mit free wieder entfernen und Heap-Speicher freigeben.

Allerdings verbrauchen solche Ketten extra-Speicher, da in jedem Glied auch der Pointer auf's nächste gespeichert werden muss. Dazu kommt der Code, der neue Glieder anhängt oder alte entfernt. Also Overhead.

Das macht also nur Sinn, wenn es wirklich wichtig ist, dass der Speicher dynamisch wachsen kann. Sollte es um eine kleine Anzahl an Elementen gehen, kannst du auch einfach ein statischen Array aufmachen. Das ist leichter und spart dir vielleicht am Ende sogar etwas Speicher. Kommt natürlich auf die Situation an.

Die Nutzung des Heap, mit der Du als erstes konfrontiert wirst, ist die in der Klasse String.

Gruß Tommy

Nein!
Du beschreibst eher eine überschreibene FiFo (first in - first out)
Ein Stack ist ein "last in - first out"
Der vergisst nichts.

Hi @vanzahl ,

erstmal Danke für diese sehr ausführliche Erklärung.
Hat mir wieder ein paar Strahlen mehr ins Dunkle gebracht.
Ich trau mich trotzdem mal noch, weiter zu fragen:

Wenn ich aber doch z.B. einer Variable wie in meinem Beispiel "int wert_fünf" außerhalb von void loop() und void test() einen Wert gebe, ist die doch AUCH in der Funktion test() verfügbar. Darum eben dachte ich, dass der dann im Heap gespeichert ist. Hier liegt mein Denkfehler bzw. meine geistige Blockade:

Ich dachte, dass der Stack immer wieder geleert wird. Da aber int wert_fünf immer verfügbar ist, wandert das ja anscheinend nie vom "Stapel"?

Und auf int wert_fünf KANN ich ja von überall aus zugreifen, oder? vom Loop aus, vom Setup aus, von void test() aus.

Soll das heißen, dass ein String IMMER automatisch im heap-Speicherbereich liegt?

Ups...das hab ich erst nach dem posten meines letzten Beitrags gelesen.
D.h. int wert_fünf ist immer noch da, weil er oben im Stack bleibt?

Hier verwechselst du die Memory Section, also den Ort, wo das Ding liegt, mit dem Geltungsbereich der Variablen, also wo du den Namen benutzen kannst.

Lesetipp: Memory Sections
und avr-libc: Memory Areas and Using malloc()

Das kannst Du Dir in den Dateien WString.h/Wstring.cpp anschauen.
Der Platz wird dynamisch alloziert.

Gruß Tommy

Also, physicalische liegen Stack und Heap auf dem selben Speicher. Das ist wie ein großes Blatt Papier. Sowohl der Stack als auch der Heap schreiben auf das selbe Blatt. Bei benutzen aber eine unterschiedliche Ecke des Papiers und strukturieren die Daten anders.

Es wäre also möglich, wenn auch nicht ratsam, über Speicher-Adressen auf Variablen zuzugreifen, auf die du eigentlich nicht zugreifen können solltest. Im Beispiel von vorhin, könntest du dann auf lokale Variablen um Stack lesen und schreiben.
Das geht aber in der Regel schief, nützt äußerst selten etwas und führt im besten Fall zum Absturz, um schlechten zu undefiniertem Verhalten, was dann bei einer Steuerung eines Motors oder sowas großen Schaden anrichten kann. Darum ist das nur ein theoretisches Beispiel und jeder sollte die Finger davon weglassen.

Combie bringt es auf den Punkt. Es gibt den Ort, wo die Daten im Speicher liegen, und es gibt die Stelle im Programm, von der man auf Variablen zugreifen kann.

Der Ort ist eigentlich egal.

Achtung, auch dünnes Eis für mich Wenn ich das bei Arduino richtig verstanden habe gilt folgendes:

  • Arduino hat 3 Speicher
    • Flash (32KB), vergleichbar zu der Festplatte und enthält das Programm, das ausgeführt wird, wird während der Laufzeit nicht verändert
    • SRAM (2KB), vergleichbar mit dem Hauptspeicher, speichert Werte während der Laufzeit des Programms
    • EEPROM (1KB), keine Ahnung
  • Konstante Variablen werden i.d.R. direkt ins Programm eingebettet und landen daher auf dem Flash, wo auch das kompilierte Programm liegt.
    • wert_sieben sollte daher auf dem Flash liegen
  • Globale Variablen liegen im SRAM, die sie geschrieben werden können.
    • Ich gehe davon aus, dass das Programm einen bestimmten Bereich für globale Variablen aus dem Heap anlegt
    • Die Anzahl der globalen Daten ist während der Kompilierzeit bekannt, d.h. es kommen während der Laufzeit keine neuen globalen Variablen dazu.
    • Das Programm kann daher einen Bereich fester Größe für globale Variablen auf dem Heap reservieren
  • static const, weiß ich leider nicht
  • lokale Variablen
    • Gelten nur innerhalb einer Funktion
    • Werden beim Aufruf einer Funktion auf dem Stack "oben drauf" angelegt
      • Neben den lokalen Variablen wird noch die Information abgelegt, von wo die Funktion aufgerufen wurde, (dahin muss das Programm springen, wenn eine Funktion beendet wird), und Platz für das Ergebnis (falls eine Funktion einen Wert zurück gibt, z.B. int).
    • Bedenke, dass es auch Rekursionen geben kann. z.B. loop -> test1 -> test2 -> test1 -> test2 -> test1. Die lokalen Variablen aus test1 existieren 3 mal auf dem Stack, die lokalen Variablen von test2 existieren doppelt auf dem Stack.
  • Variablen, die mit malloc erzeugt werden, liegen auf dem Heap
    • Und da liegen sie auch, bis zum Neustart, oder wenn sie mit free aufgeräumt werden.

Meistens ist es aber wirklich egal, wo die Daten wie liegen. Klar muss dir nur sein:

  • Was ich mit malloc anlege, muss ich mit free aufräumen
  • Lokal heißt lokal und ich kann auch nur innerhalb der selben Bereich darauf zugreifen

Alles andere, eher ja...
Aber: Das nicht.
Der Heap beginnt nach den globalen/statischen Variablen.
Da gibts also keine Überschneidung.



Es gibt noch eine Besonderheit, in Bezug auf Stack und Heap.

Beide wachsen aufeinander zu!
Wobei aber für den Heap eine obere Grenze festgelegt ist, ab der malloc() und seine Freunde einen Null Zeiger zurückliefen.
Für den Stack gibt es allerdings von hause aus keinen solchen Schutzmechanismus.
Wobei z.B. AVR FreeRTOS das auch erkennen kann.
Nicht vermeiden, aber im nachhinein erkennen, was dann in einem unmittelbaren Halt oder Reset münden sollte.

Ab der mittleren 32Bit µC Klasse finden sich MMUs, welche bei Bereichsüberschreitungen einen hard fault auslösen. können

Auf den kleinen µC kann der Stack also versehentlich den Heap überschreiben (z.B. Rekursion), aber der Heap nicht in den Stackbereich ragen.

@vanzahl auch dir rate ich den Link den ich in #5 gepostet habe mal zu lesen.

Ihr seid super, Danke euch! :hugs: