Was und wo sind der Stapel und der Haufen?


8103

In Programmiersprachenbüchern wird erklärt, dass Werttypen auf dem Stapel und Referenztypen auf dem Heap erstellt werden , ohne zu erklären, was diese beiden Dinge sind. Ich habe keine klare Erklärung dafür gelesen. Ich verstehe, was ein Stapel ist. Aber,

  • Wo und was sind sie (physisch im Speicher eines echten Computers)?
  • Inwieweit werden sie vom Betriebssystem oder der Sprachlaufzeit gesteuert?
  • Was ist ihr Umfang?
  • Was bestimmt die Größe jedes einzelnen von ihnen?
  • Was macht einen schneller?

175
Eine wirklich gute Erklärung finden Sie hier. Was ist der Unterschied zwischen einem Stapel und einem Haufen?
Songo

12
Auch (wirklich) gut: codeproject.com/Articles/76153/… (der Stapel- / Heap-Teil)
Ben


3
Siehe auch Stack Clash . Die Stack Clash-Korrekturen wirkten sich auf einige Aspekte von Systemvariablen und Verhaltensweisen aus, z rlimit_stack. Siehe auch Red Hat Issue 1463241
jww

3
@mattshane Die Definitionen von Stack und Heap hängen nicht von Wert- und Referenztypen ab. Mit anderen Worten, der Stapel und der Heap können vollständig definiert werden, selbst wenn Wert- und Referenztypen nie vorhanden waren. Wenn Sie Wert- und Referenztypen verstehen, ist der Stapel nur ein Implementierungsdetail. Per Eric Lippert: Der Stapel ist ein Implementierungsdetail, Teil Eins .
Matthew

Antworten:


5964

Der Stapel ist der Speicher, der als Arbeitsbereich für einen Ausführungsthread reserviert ist. Wenn eine Funktion aufgerufen wird, wird oben im Stapel ein Block für lokale Variablen und einige Buchhaltungsdaten reserviert. Wenn diese Funktion zurückkehrt, wird der Block nicht mehr verwendet und kann beim nächsten Aufruf einer Funktion verwendet werden. Der Stapel wird immer in einer LIFO-Reihenfolge (last in first out) reserviert. Der zuletzt reservierte Block ist immer der nächste freizugebende Block. Dies macht es wirklich einfach, den Stapel im Auge zu behalten. Das Befreien eines Blocks vom Stapel ist nichts anderes als das Anpassen eines Zeigers.

Der Heap ist Speicher, der für die dynamische Zuordnung reserviert ist. Im Gegensatz zum Stapel gibt es kein erzwungenes Muster für die Zuweisung und Freigabe von Blöcken aus dem Heap. Sie können einen Block jederzeit zuweisen und jederzeit freigeben. Dies macht es viel komplexer, zu verfolgen, welche Teile des Heaps zu einem bestimmten Zeitpunkt zugewiesen oder frei sind. Es stehen viele benutzerdefinierte Heap-Allokatoren zur Verfügung, um die Heap-Leistung für verschiedene Verwendungsmuster zu optimieren.

Jeder Thread erhält einen Stapel, während es normalerweise nur einen Heap für die Anwendung gibt (obwohl es nicht ungewöhnlich ist, mehrere Heaps für verschiedene Zuordnungstypen zu haben).

So beantworten Sie Ihre Fragen direkt:

Inwieweit werden sie vom Betriebssystem oder der Sprachlaufzeit gesteuert?

Das Betriebssystem weist den Stapel jedem Thread auf Systemebene zu, wenn der Thread erstellt wird. In der Regel wird das Betriebssystem von der Sprachlaufzeit aufgerufen, um den Heap für die Anwendung zuzuweisen.

Was ist ihr Umfang?

Der Stapel ist an einen Thread angehängt. Wenn der Thread beendet wird, wird der Stapel zurückgefordert. Der Heap wird normalerweise beim Start der Anwendung zur Laufzeit zugewiesen und beim Beenden der Anwendung (technisch verarbeitet) zurückgefordert.

Was bestimmt die Größe jedes einzelnen von ihnen?

Die Größe des Stapels wird festgelegt, wenn ein Thread erstellt wird. Die Größe des Heapspeichers wird beim Start der Anwendung festgelegt, kann jedoch bei Bedarf an Speicherplatz zunehmen (der Allokator fordert mehr Speicher vom Betriebssystem an).

Was macht einen schneller?

Der Stapel ist schneller, da das Zugriffsmuster das Zuweisen und Freigeben von Speicher von ihm trivial macht (ein Zeiger / eine Ganzzahl wird einfach inkrementiert oder dekrementiert), während der Heap eine viel komplexere Buchhaltung bei einer Zuweisung oder Freigabe aufweist. Außerdem wird jedes Byte im Stapel sehr häufig wiederverwendet, was bedeutet, dass es dem Cache des Prozessors zugeordnet wird, was es sehr schnell macht. Ein weiterer Leistungseinbruch für den Heap besteht darin, dass der Heap, der hauptsächlich eine globale Ressource ist, in der Regel multithreading-sicher sein muss, dh jede Zuweisung und Freigabe muss - normalerweise - mit "allen" anderen Heap-Zugriffen im Programm synchronisiert werden.

Eine klare Demonstration:
Bildquelle: vikashazrati.wordpress.com


74
Gute Antwort - aber ich denke, Sie sollten hinzufügen, dass der Stapel zwar vom Betriebssystem zugewiesen wird, wenn der Prozess startet (vorausgesetzt, es gibt ein Betriebssystem), aber vom Programm inline verwaltet wird. Dies ist ein weiterer Grund, warum der Stapel auch schneller ist - Push- und Pop-Operationen sind normalerweise eine Maschinenanweisung, und moderne Maschinen können mindestens drei davon in einem Zyklus ausführen, während das Zuweisen oder Freigeben von Heap das Aufrufen von Betriebssystemcode umfasst.
sqykly

276
Das Diagramm am Ende verwirrt mich wirklich. Ich dachte, ich hätte es verstanden, bis ich das Bild sah.
Sina Madani

10
@Anarelle Der Prozessor führt Anweisungen mit oder ohne Betriebssystem aus. Ein Beispiel, das mir am Herzen liegt, ist das SNES, das keine API-Aufrufe und kein Betriebssystem hatte, wie wir es heute kennen - aber es hatte einen Stapel. Das Zuweisen auf einem Stapel ist Addition und Subtraktion auf diesen Systemen, und das ist in Ordnung für Variablen, die zerstört werden, wenn sie durch Zurückkehren von der Funktion, die sie erstellt hat, gelöscht werden, aber dies beispielsweise auf einen Konstruktor beschränken, dessen Ergebnis nicht einfach sein kann weggeworfen. Dafür brauchen wir den Haufen, der nicht gebunden ist, um anzurufen und zurückzukehren. Die meisten Betriebssysteme haben APIs einen Haufen, kein Grund, es selbst zu tun
sqykly

2
"Stapel ist der Speicher, der als Arbeitsbereich reserviert ist". Cool. Aber wo ist es eigentlich in Bezug auf die Java-Speicherstruktur "beiseite gelegt"? Ist es Heap-Speicher / Nicht-Heap-Speicher / Andere (Java-Speicherstruktur gemäß betsol.com/2017/06/… )
Jatin Shashoo

4
@JatinShashoo Java-Laufzeit als Bytecode-Interpreter fügt eine weitere Virtualisierungsebene hinzu. Sie haben also nur die Sichtweise der Java-Anwendung erwähnt. Aus Sicht des Betriebssystems ist dies alles nur ein Heap, bei dem der Java-Laufzeitprozess einen Teil seines Speicherplatzes als "Nicht-Heap" -Speicher für verarbeiteten Bytecode zuweist. Der Rest dieses Heaps auf Betriebssystemebene wird als Heap auf Anwendungsebene verwendet, in dem die Daten des Objekts gespeichert werden.
Kbec

2349

Stapel:

  • Wie der Heap im Computer-RAM gespeichert.
  • Auf dem Stapel erstellte Variablen verlassen den Gültigkeitsbereich und werden automatisch freigegeben.
  • Im Vergleich zu Variablen auf dem Heap viel schneller zuzuweisen.
  • Implementiert mit einer tatsächlichen Stack-Datenstruktur.
  • Speichert lokale Daten, Rücksprungadressen, die für die Parameterübergabe verwendet werden.
  • Kann einen Stapelüberlauf haben, wenn zu viel des Stapels verwendet wird (meistens aufgrund einer unendlichen oder zu tiefen Rekursion, sehr großer Zuordnungen).
  • Auf dem Stapel erstellte Daten können ohne Zeiger verwendet werden.
  • Sie würden den Stapel verwenden, wenn Sie genau wissen, wie viele Daten Sie vor der Kompilierungszeit zuweisen müssen und er nicht zu groß ist.
  • Normalerweise hat eine maximale Größe bereits festgelegt, wenn Ihr Programm startet.

Haufen:

  • Wird wie der Stack im Computer-RAM gespeichert.
  • In C ++ müssen Variablen auf dem Heap manuell zerstört werden und dürfen niemals außerhalb des Gültigkeitsbereichs liegen. Die Daten werden mit befreit delete, delete[]oder free.
  • Langsameres Zuweisen im Vergleich zu Variablen auf dem Stapel.
  • Wird bei Bedarf verwendet, um einen Datenblock zur Verwendung durch das Programm zuzuweisen.
  • Kann fragmentiert sein, wenn es viele Zuweisungen und Freigaben gibt.
  • In C ++ oder C werden auf dem Heap erstellte Daten durch Zeiger angezeigt und mit newbzw. zugeordnet malloc.
  • Kann zu Zuordnungsfehlern führen, wenn ein zu großer Puffer für die Zuweisung angefordert wird.
  • Sie würden den Heap verwenden, wenn Sie nicht genau wissen, wie viele Daten Sie zur Laufzeit benötigen oder wenn Sie viele Daten zuweisen müssen.
  • Verantwortlich für Speicherlecks.

Beispiel:

int foo()
{
  char *pBuffer; //<--nothing allocated yet (excluding the pointer itself, which is allocated here on the stack).
  bool b = true; // Allocated on the stack.
  if(b)
  {
    //Create 500 bytes on the stack
    char buffer[500];

    //Create 500 bytes on the heap
    pBuffer = new char[500];

   }//<-- buffer is deallocated here, pBuffer is not
}//<--- oops there's a memory leak, I should have called delete[] pBuffer;

31
Der Zeiger pBuffer und der Wert von b befinden sich auf dem Stapel und werden meistens am Eingang der Funktion zugewiesen. Je nach Compiler kann der Puffer auch am Funktionseingang zugewiesen werden.
Andy

36
Es ist ein weit verbreitetes Missverständnis, dass die CSprache, wie sie im C99Sprachstandard definiert ist (verfügbar unter open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf ), einen "Stapel" erfordert. Tatsächlich kommt das Wort "Stapel" nicht einmal im Standard vor. Dies beantwortet Aussagen zur Stapelverwendung von wrt / to C, die im Allgemeinen wahr sind, aber in keiner Weise von der Sprache verlangt werden. Weitere Informationen finden Sie unter knosof.co.uk/cbook/cbook.html und insbesondere Cin der Implementierung auf Odd-Ball-Architekturen wie en.wikipedia.org/wiki/Burroughs_large_systems
johne

55
@Brian Sie sollten erklären, warum buffer [] und der pBuffer-Zeiger auf dem Stapel erstellt werden und warum die Daten von pBuffer auf dem Heap erstellt werden. Ich denke, einige Leute könnten durch Ihre Antwort verwirrt sein, da sie denken könnten, dass das Programm speziell anweist, dass Speicher auf dem Stapel gegen Heap zugewiesen wird, aber dies ist nicht der Fall. Liegt es daran, dass Buffer ein Werttyp ist, während pBuffer ein Referenztyp ist?
Howiecamp

9
@Remover: Nein, ein Zeiger enthält eine Adresse und kann gleichermaßen auf etwas auf dem Heap oder dem Stapel verweisen. new, malloc und einige andere Funktionen, die malloc ähneln, weisen den Heap zu und geben die Adresse des zugewiesenen Speichers zurück. Warum sollten Sie auf dem Heap zuordnen wollen? Damit Ihr Speicher nicht aus dem Rahmen gerät und freigegeben wird, bis Sie es möchten.
Brian R. Bondy

35
"Verantwortlich für Speicherlecks" - Heaps sind nicht verantwortlich für Speicherlecks! Lazy / Forgetful / Ex-Java-Codierer / Codierer, die keinen Mist geben, sind!
Laz

1370

Der wichtigste Punkt ist, dass Heap und Stack allgemeine Begriffe für die Zuweisung von Speicher sind. Sie können auf viele verschiedene Arten implementiert werden, und die Begriffe gelten für die Grundkonzepte.

  • In einem Stapel von Gegenständen liegen die Gegenstände in der Reihenfolge, in der sie dort platziert wurden, übereinander, und Sie können nur den obersten entfernen (ohne das Ganze umzustürzen).

    Stapel wie ein Stapel Papiere

    Die Einfachheit eines Stapels besteht darin, dass Sie keine Tabelle verwalten müssen, die einen Datensatz für jeden Abschnitt des zugewiesenen Speichers enthält. Die einzige Statusinformation, die Sie benötigen, ist ein einzelner Zeiger auf das Ende des Stapels. Zum Zuweisen und Aufheben der Zuweisung erhöhen und verringern Sie einfach diesen einzelnen Zeiger. Hinweis: Manchmal kann ein Stapel implementiert werden, der am oberen Rand eines Speicherabschnitts beginnt und sich nach unten erstreckt, anstatt nach oben zu wachsen.

  • In einem Haufen gibt es keine bestimmte Reihenfolge für die Platzierung von Elementen. Sie können Artikel in beliebiger Reihenfolge erreichen und entfernen, da es keinen eindeutigen "Top" -Element gibt.

    Haufen wie ein Haufen Lakritz aller Art

    Die Heap-Zuweisung erfordert die vollständige Aufzeichnung, welcher Speicher zugewiesen ist und was nicht, sowie einige Overhead-Wartungsarbeiten, um die Fragmentierung zu verringern, zusammenhängende Speichersegmente zu finden, die groß genug sind, um der angeforderten Größe zu entsprechen, und so weiter. Der Speicher kann jederzeit freigegeben werden, sodass freier Speicherplatz verbleibt. Manchmal führt ein Speicherzuweiser Wartungsaufgaben aus, z. B. das Defragmentieren des Speichers durch Verschieben des zugewiesenen Speichers oder das Sammeln von Speicherplatz. Er identifiziert zur Laufzeit, wenn der Speicher nicht mehr im Umfang ist, und gibt die Zuordnung auf.

Diese Bilder sollten die beiden Möglichkeiten zum Zuweisen und Freigeben von Speicher in einem Stapel und einem Heap recht gut beschreiben. Yum!

  • Inwieweit werden sie vom Betriebssystem oder der Sprachlaufzeit gesteuert?

    Wie bereits erwähnt, sind Heap und Stack allgemeine Begriffe und können auf viele Arten implementiert werden. Computerprogramme haben normalerweise einen Stapel, der als Aufrufstapel bezeichnet wird und Informationen speichert, die für die aktuelle Funktion relevant sind, wie z. B. einen Zeiger auf die Funktion, von der aus sie aufgerufen wurden, und lokale Variablen. Da Funktionen andere Funktionen aufrufen und dann zurückkehren, wächst und schrumpft der Stapel, um Informationen von den Funktionen weiter unten im Aufrufstapel zu speichern. Ein Programm hat nicht wirklich die Laufzeitsteuerung. Es wird von der Programmiersprache, dem Betriebssystem und sogar der Systemarchitektur bestimmt.

    Ein Heap ist ein allgemeiner Begriff für jeden Speicher, der dynamisch und zufällig zugewiesen wird. dh außer Betrieb. Der Speicher wird normalerweise vom Betriebssystem zugewiesen, wobei die Anwendung API-Funktionen aufruft, um diese Zuordnung vorzunehmen. Die Verwaltung des dynamisch zugewiesenen Speichers erfordert einiges an Overhead, das normalerweise vom Laufzeitcode der verwendeten Programmiersprache oder -umgebung verwaltet wird.

  • Was ist ihr Umfang?

    Der Aufrufstapel ist ein so niedriges Konzept, dass er sich nicht auf "Umfang" im Sinne der Programmierung bezieht. Wenn Sie Code zerlegen, werden relative Verweise auf Zeigerstile auf Teile des Stapels angezeigt. In Bezug auf eine übergeordnete Sprache legt die Sprache jedoch ihre eigenen Bereichsregeln fest. Ein wichtiger Aspekt eines Stapels ist jedoch, dass nach der Rückkehr einer Funktion alles, was für diese Funktion lokal ist, sofort vom Stapel freigegeben wird. Das funktioniert so, wie Sie es erwarten würden, wenn man bedenkt, wie Ihre Programmiersprachen funktionieren. In einem Haufen ist es auch schwierig zu definieren. Der Bereich ist das, was auch immer vom Betriebssystem verfügbar gemacht wird, aber Ihre Programmiersprache fügt wahrscheinlich die Regeln hinzu, was ein "Bereich" in Ihrer Anwendung ist. Die Prozessorarchitektur und das Betriebssystem verwenden die virtuelle Adressierung. was der Prozessor in physische Adressen übersetzt und es gibt Seitenfehler usw. Sie verfolgen, welche Seiten zu welchen Anwendungen gehören. Sie müssen sich darüber jedoch nie wirklich Gedanken machen, da Sie nur die Methode verwenden, die Ihre Programmiersprache verwendet, um Speicher zuzuweisen und freizugeben, und nach Fehlern suchen (wenn die Zuweisung / Freigabe aus irgendeinem Grund fehlschlägt).

  • Was bestimmt die Größe jedes einzelnen von ihnen?

    Auch dies hängt von der Sprache, dem Compiler, dem Betriebssystem und der Architektur ab. Ein Stapel ist normalerweise vorab zugewiesen, da er per Definition ein zusammenhängender Speicher sein muss. Der Sprachcompiler oder das Betriebssystem bestimmen seine Größe. Sie speichern keine großen Datenmengen auf dem Stapel, daher ist er groß genug, um niemals vollständig verwendet zu werden, außer in Fällen unerwünschter endloser Rekursion (daher "Stapelüberlauf") oder anderer ungewöhnlicher Programmierentscheidungen.

    Ein Heap ist ein allgemeiner Begriff für alles, was dynamisch zugewiesen werden kann. Je nachdem, wie Sie es betrachten, ändert sich die Größe ständig. In modernen Prozessoren und Betriebssystemen ist die genaue Funktionsweise ohnehin sehr abstrahiert, sodass Sie sich normalerweise keine Gedanken darüber machen müssen, wie es tief im Inneren funktioniert, außer dass Sie (in Sprachen, in denen Sie dies zulassen) keinen Speicher verwenden dürfen Sie haben noch keinen Speicher zugewiesen, den Sie freigegeben haben.

  • Was macht einen schneller?

    Der Stapel ist schneller, da der gesamte freie Speicher immer zusammenhängend ist. Es muss keine Liste aller Segmente des freien Speichers geführt werden, nur ein einziger Zeiger auf den aktuellen oberen Rand des Stapels. Compiler speichern diesen Zeiger normalerweise zu diesem Zweck in einem speziellen, schnellen Register . Darüber hinaus konzentrieren sich nachfolgende Operationen auf einem Stapel normalerweise auf sehr nahe gelegene Speicherbereiche, was auf einer sehr niedrigen Ebene für die Optimierung durch die On-Die-Caches des Prozessors gut ist.


20
David Ich stimme nicht zu, dass dies ein gutes Image ist oder dass "Push-Down-Stack" ein guter Begriff ist, um das Konzept zu veranschaulichen. Wenn Sie einem Stapel etwas hinzufügen, werden die anderen Inhalte des Stapels nicht nach unten gedrückt, sondern bleiben dort, wo sie sind.
Thomasrutter

8
Diese Antwort enthält einen großen Fehler. Statische Variablen werden dem Stapel nicht zugeordnet. Weitere Informationen finden Sie in meiner Antwort [Link] stackoverflow.com/a/13326916/1763801 . Sie setzen "automatische" Variablen mit "statischen" Variablen gleich, aber sie sind überhaupt nicht gleich
Davec

13
Insbesondere sagen Sie, dass "statisch zugewiesene lokale Variablen" auf dem Stapel zugewiesen sind. Tatsächlich sind sie im Datensegment zugeordnet. Auf dem Stapel werden nur automatisch zugewiesene Variablen zugewiesen (die die meisten, aber nicht alle lokalen Variablen sowie Funktionsparameter enthalten, die nicht nach Referenz, sondern nach Wert übergeben werden).
Davec

9
Ich habe gerade festgestellt, dass Sie Recht haben - in C ist die statische Zuordnung eher eine eigenständige Sache als ein Begriff für etwas, das nicht dynamisch ist . Ich habe meine Antwort bearbeitet, danke.
Thomasrutter

5
Es ist nicht nur C. Java, Pascal, Python und viele andere haben alle die Begriffe statische versus automatische versus dynamische Zuordnung. "Statische Zuordnung" zu sagen bedeutet fast überall dasselbe. In keiner Sprache bedeutet statische Zuordnung "nicht dynamisch". Sie möchten den Begriff "automatische" Zuordnung für das, was Sie beschreiben (dh die Dinge auf dem Stapel).
Davec

727

(Ich habe diese Antwort von einer anderen Frage verschoben, die mehr oder weniger ein Betrüger dieser Frage war.)

Die Antwort auf Ihre Frage ist implementierungsspezifisch und kann je nach Compiler und Prozessorarchitektur variieren. Hier ist jedoch eine vereinfachte Erklärung.

  • Sowohl der Stapel als auch der Heap sind Speicherbereiche, die vom zugrunde liegenden Betriebssystem zugewiesen werden (häufig virtueller Speicher, der bei Bedarf dem physischen Speicher zugeordnet wird).
  • In einer Umgebung mit mehreren Threads verfügt jeder Thread über einen eigenen, völlig unabhängigen Stapel, der sich jedoch den Heap teilt. Der gleichzeitige Zugriff muss auf dem Heap gesteuert werden und ist auf dem Stapel nicht möglich.

Der Haufen

  • Der Heap enthält eine verknüpfte Liste der verwendeten und freien Blöcke. Neue Zuordnungen auf dem Heap (von newoder malloc) werden erfüllt, indem aus einem der freien Blöcke ein geeigneter Block erstellt wird. Dies erfordert die Aktualisierung der Liste der Blöcke auf dem Heap. Diese Metainformationen zu den Blöcken auf dem Heap werden auch häufig in einem kleinen Bereich direkt vor jedem Block auf dem Heap gespeichert.
  • Wenn der Heap wächst, werden häufig neue Blöcke von niedrigeren Adressen zu höheren Adressen zugewiesen. Sie können sich den Heap also als einen Haufen von Speicherblöcken vorstellen, dessen Größe mit der Zuweisung von Speicher zunimmt. Wenn der Heap für eine Zuordnung zu klein ist, kann die Größe häufig erhöht werden, indem mehr Speicher vom zugrunde liegenden Betriebssystem abgerufen wird.
  • Das Zuweisen und Freigeben vieler kleiner Blöcke kann den Heap in einem Zustand belassen, in dem viele kleine freie Blöcke zwischen den verwendeten Blöcken verteilt sind. Eine Anforderung zum Zuweisen eines großen Blocks kann fehlschlagen, da keiner der freien Blöcke groß genug ist, um die Zuweisungsanforderung zu erfüllen, obwohl die kombinierte Größe der freien Blöcke möglicherweise groß genug ist. Dies wird als Heap-Fragmentierung bezeichnet .
  • Wenn ein verwendeter Block, der an einen freien Block angrenzt, freigegeben wird, kann der neue freie Block mit dem benachbarten freien Block zusammengeführt werden, um einen größeren freien Block zu erzeugen, der die Fragmentierung des Heaps effektiv verringert.

Der Haufen

Der Stapel

  • Der Stapel arbeitet häufig eng zusammen mit einem speziellen Register auf der CPU, das als Stapelzeiger bezeichnet wird . Zu Beginn zeigt der Stapelzeiger auf die Oberseite des Stapels (die höchste Adresse auf dem Stapel).
  • Die CPU verfügt über spezielle Anweisungen für die Schubwerte auf den Stapel und knallen sie wieder vom Stapel. Jeder Push speichert den Wert an der aktuellen Position des Stapelzeigers und verringert den Stapelzeiger. Ein Popup ruft den Wert ab, auf den der Stapelzeiger zeigt, und erhöht dann den Stapelzeiger (nicht zu verwechseln durch die Tatsache, dass das Hinzufügen eines Werts zum Stapel den Stapelzeiger verringert und das Entfernen eines Werts ihn erhöht . Denken Sie daran, dass der Stapel wächst der Boden). Die gespeicherten und abgerufenen Werte sind die Werte der CPU-Register.
  • Wenn eine Funktion aufgerufen wird, verwendet die CPU spezielle Anweisungen, die den aktuellen Anweisungszeiger drücken , dh die Adresse des Codes, der auf dem Stapel ausgeführt wird. Die CPU springt dann zur Funktion, indem sie den Befehlszeiger auf die Adresse der aufgerufenen Funktion setzt. Später, wenn die Funktion zurückkehrt, wird der alte Anweisungszeiger vom Stapel entfernt und die Ausführung wird unmittelbar nach dem Aufruf der Funktion am Code fortgesetzt.
  • Wenn eine Funktion eingegeben wird, wird der Stapelzeiger verringert, um mehr Platz auf dem Stapel für lokale (automatische) Variablen zuzuweisen. Wenn die Funktion eine lokale 32-Bit-Variable hat, werden vier Bytes auf dem Stapel beiseite gelegt. Wenn die Funktion zurückkehrt, wird der Stapelzeiger zurückbewegt, um den zugewiesenen Bereich freizugeben.
  • Wenn eine Funktion Parameter hat, werden diese vor dem Aufruf der Funktion auf den Stapel verschoben. Der Code in der Funktion kann dann vom aktuellen Stapelzeiger nach oben navigieren, um diese Werte zu lokalisieren.
  • Verschachtelungsfunktionsaufrufe wirken wie ein Zauber. Jeder neue Aufruf weist Funktionsparameter, die Rücksprungadresse und den Speicherplatz für lokale Variablen zu. Diese Aktivierungsdatensätze können für verschachtelte Aufrufe gestapelt werden und werden bei der Rückkehr der Funktionen auf die richtige Weise abgewickelt.
  • Da der Stapel ein begrenzter Speicherblock ist, können Sie einen Stapelüberlauf verursachen, indem Sie zu viele verschachtelte Funktionen aufrufen und / oder zu viel Speicherplatz für lokale Variablen zuweisen. Oft ist der für den Stapel verwendete Speicherbereich so eingerichtet, dass das Schreiben unter den unteren Rand (die niedrigste Adresse) des Stapels einen Trap oder eine Ausnahme in der CPU auslöst. Diese Ausnahmebedingung kann dann von der Laufzeit erfasst und in eine Art Stapelüberlaufausnahme umgewandelt werden.

Der Stapel

Kann eine Funktion auf dem Heap anstelle eines Stapels zugewiesen werden?

Nein, Aktivierungsdatensätze für Funktionen (dh lokale oder automatische Variablen) werden auf dem Stapel zugewiesen, der nicht nur zum Speichern dieser Variablen, sondern auch zum Verfolgen verschachtelter Funktionsaufrufe verwendet wird.

Wie der Heap verwaltet wird, hängt wirklich von der Laufzeitumgebung ab. C verwendet mallocund C ++ verwendet new, aber viele andere Sprachen haben Garbage Collection.

Der Stapel ist jedoch ein Merkmal auf niedrigerer Ebene, das eng mit der Prozessorarchitektur verbunden ist. Das Wachsen des Heaps, wenn nicht genügend Speicherplatz vorhanden ist, ist nicht allzu schwierig, da es in dem Bibliotheksaufruf implementiert werden kann, der den Heap verarbeitet. Das Wachsen des Stapels ist jedoch häufig nicht möglich, da der Stapelüberlauf nur dann erkannt wird, wenn es zu spät ist. Das Herunterfahren des Ausführungsthreads ist die einzig praktikable Option.


35
@ Martin - Eine sehr gute Antwort / Erklärung als die abstrakter akzeptierte Antwort. Ein Beispiel für ein Assembler-Programm, das Stapelzeiger / -register zeigt, die für Funktionsaufrufe verwendet werden, wäre anschaulicher.
Bikal Lem

3
Jeder Referenztyp besteht aus Werttypen (int, string usw.). Wie gesagt, werden Werttypen im Stapel gespeichert, als wie es funktioniert, wenn sie Teil des Referenztyps sind.
Nps

15
Diese Antwort war meiner Meinung nach die beste, weil sie mir geholfen hat zu verstehen, was eine Rückgabeanweisung wirklich ist und wie sie sich auf diese "Rücksprungadresse" bezieht, auf die ich hin und wieder stoße, was es bedeutet, eine Funktion auf den Stapel zu schieben. und warum Funktionen auf Stapel geschoben werden. Gute Antwort!
Alex

3
Dies ist meiner Meinung nach das Beste, um zu erwähnen, dass der Heap / Stack sehr implementierungsspezifisch ist. Die anderen Antworten setzen viele Dinge in Bezug auf die Sprache und die Umgebung / das Betriebssystem voraus . +1
Qix - MONICA wurde

2
Was meinen Sie damit? "Der Code in der Funktion kann dann vom aktuellen Stapelzeiger nach oben navigieren, um diese Werte zu finden." ? Können Sie das bitte näher erläutern?
Koray Tugay

404

Im folgenden C # -Code

public void Method1()
{
    int i = 4;
    int y = 2;
    class1 cls1 = new class1();
}

So wird der Speicher verwaltet

Bild der Variablen auf dem Stapel

Local VariablesDas muss nur so lange dauern, wie der Funktionsaufruf im Stapel abläuft. Der Heap wird für Variablen verwendet, deren Lebensdauer wir nicht wirklich kennen, aber wir erwarten, dass sie eine Weile dauern. In den meisten Sprachen ist es wichtig, dass wir zur Kompilierungszeit wissen, wie groß eine Variable ist, wenn wir sie auf dem Stapel speichern möchten.

Objekte (deren Größe beim Aktualisieren variiert) werden auf dem Heap gespeichert, da wir zum Zeitpunkt der Erstellung nicht wissen, wie lange sie dauern werden. In vielen Sprachen wird der Heap durch Müll gesammelt, um Objekte (wie das Objekt cls1) zu finden, die keine Referenzen mehr haben.

In Java werden die meisten Objekte direkt in den Heap verschoben. In Sprachen wie C / C ++ können Strukturen und Klassen häufig auf dem Stapel verbleiben, wenn Sie nicht mit Zeigern arbeiten.

Weitere Informationen finden Sie hier:

Der Unterschied zwischen Stapel- und Heapspeicherzuordnung «timmurphy.org

und hier:

Erstellen von Objekten auf dem Stapel und dem Heap

Dieser Artikel ist die Quelle des obigen Bildes: Sechs wichtige .NET-Konzepte: Stapel, Heap, Werttypen, Referenztypen, Boxen und Unboxing - CodeProject

Beachten Sie jedoch, dass es einige Ungenauigkeiten enthalten kann.


15
Das ist falsch. i und cls sind keine "statischen" Variablen. Sie werden als "lokale" oder "automatische" Variablen bezeichnet. Es ist eine sehr wichtige Unterscheidung. Siehe [Link] stackoverflow.com/a/13326916/1763801 für Klarstellung
Davec

9
Ich habe nicht gesagt, dass es sich um statische Variablen handelt . Ich sagte, dass int und cls1 statische Elemente sind . Ihr Speicher ist statisch zugeordnet und daher werden sie auf den Stapel gelegt. Dies steht im Gegensatz zu einem Objekt, das eine dynamische Speicherzuweisung erfordert, die sich daher auf dem Heap befindet.
Snowcrash

12
Ich zitiere "Statische Gegenstände ... geh auf den Stapel". Das ist einfach falsch. Statische Elemente werden in das Datensegment verschoben, automatische Elemente werden in den Stapel aufgenommen.
Davec

14
Auch wer diesen Codeprojekt-Artikel geschrieben hat, weiß nicht, wovon er spricht. Zum Beispiel sagt er, "primitive brauchen statischen Speicher", was völlig falsch ist. Nichts hindert Sie daran, Grundelemente im Heap dynamisch zuzuweisen. Schreiben Sie einfach etwas wie "int array [] = new int [num]" und voila, Grundelemente, die in .NET dynamisch zugewiesen werden. Das ist nur eine von mehreren Ungenauigkeiten.
Davec

8
Ich habe Ihren Beitrag bearbeitet, weil Sie schwerwiegende technische Fehler in Bezug auf den Stapel und den Haufen gemacht haben.
Tom Leys

209

Der Stapel Wenn Sie eine Funktion aufrufen, werden die Argumente für diese Funktion sowie ein anderer Overhead auf den Stapel gelegt. Dort werden auch einige Informationen gespeichert (z. B. wohin Sie bei der Rückgabe gehen müssen). Wenn Sie eine Variable in Ihrer Funktion deklarieren, wird diese Variable auch dem Stapel zugewiesen.

Die Freigabe des Stapels ist ziemlich einfach, da Sie die Zuordnung immer in der umgekehrten Reihenfolge aufheben, in der Sie sie zuweisen. Beim Eingeben von Funktionen werden Stapelmaterial hinzugefügt, die entsprechenden Daten werden beim Beenden entfernt. Dies bedeutet, dass Sie dazu neigen, in einem kleinen Bereich des Stapels zu bleiben, es sei denn, Sie rufen viele Funktionen auf, die viele andere Funktionen aufrufen (oder erstellen eine rekursive Lösung).

Der Heap Der Heap ist ein generischer Name für den Ort, an dem Sie die von Ihnen erstellten Daten im laufenden Betrieb ablegen. Wenn Sie nicht wissen, wie viele Raumschiffe Ihr Programm erstellen wird, verwenden Sie wahrscheinlich den neuen (oder malloc oder einen gleichwertigen) Operator, um jedes Raumschiff zu erstellen. Diese Zuordnung wird noch eine Weile bestehen bleiben, daher werden wir die Dinge wahrscheinlich in einer anderen Reihenfolge freigeben, als wir sie erstellt haben.

Daher ist der Heap weitaus komplexer, da es am Ende Speicherbereiche gibt, die nicht verwendet werden, verschachtelt mit Blöcken, die - Speicher wird fragmentiert. Es ist ein schwieriges Problem, freien Speicher in der Größe zu finden, die Sie benötigen. Aus diesem Grund sollte der Heap vermieden werden (obwohl er immer noch häufig verwendet wird).

Implementierung Die Implementierung von Stack und Heap hängt normalerweise von der Laufzeit / dem Betriebssystem ab. Oft erstellen Spiele und andere Anwendungen, die leistungskritisch sind, ihre eigenen Speicherlösungen, die einen großen Teil des Speichers aus dem Heap holen und ihn dann intern austeilen, um nicht auf das Betriebssystem als Speicher angewiesen zu sein.

Dies ist nur dann praktisch, wenn Ihre Speichernutzung stark von der Norm abweicht - dh für Spiele, bei denen Sie in einem großen Vorgang ein Level laden und in einem anderen großen Vorgang das ganze Los wegwerfen können.

Physischer Speicherort Dies ist weniger relevant als Sie denken, da eine Technologie namens Virtual Memory Ihr Programm den Eindruck erweckt, dass Sie Zugriff auf eine bestimmte Adresse haben, an der sich die physischen Daten an einem anderen Ort befinden (sogar auf der Festplatte!). Die Adressen, die Sie für den Stapel erhalten, werden in aufsteigender Reihenfolge angezeigt, wenn Ihr Aufrufbaum tiefer wird. Die Adressen für den Heap sind nicht vorhersehbar (dh implementierungsspezifisch) und offen gesagt nicht wichtig.


16
Eine Empfehlung, die Verwendung des Heaps zu vermeiden, ist ziemlich stark. Moderne Systeme haben gute Heap-Manager, und moderne dynamische Sprachen verwenden den Heap ausgiebig (ohne dass sich der Programmierer wirklich darum kümmert). Ich würde sagen, benutze den Haufen, aber vergiss nicht, ihn mit einem manuellen Allokator freizugeben!
Greg Hewgill

2
Wenn Sie den Stapel oder den Heap verwenden können, verwenden Sie den Stapel. Wenn Sie den Stapel nicht verwenden können, haben Sie wirklich keine Wahl. Ich benutze beides viel und natürlich std :: vector oder ähnliches trifft den Haufen. Für einen Anfänger meiden Sie den Haufen, weil der Stapel einfach so einfach ist !!
Tom Leys

Wenn Ihre Sprache keine Speicherbereinigung implementiert, sind intelligente Zeiger (separat zugewiesene Objekte, die einen Zeiger umschließen, der die Referenzzählung für dynamisch zugewiesene Speicherblöcke durchführt) eng mit der Speicherbereinigung verbunden und bieten eine gute Möglichkeit, den Heap in einem Safe zu verwalten und leckagefrei. Sie werden in verschiedenen Frameworks implementiert, sind aber auch für Ihre eigenen Programme nicht so schwer zu implementieren.
BenPen

1
"Deshalb sollte der Haufen vermieden werden (obwohl er immer noch häufig verwendet wird)." Ich bin mir nicht sicher, was dies praktisch bedeutet, zumal der Speicher in vielen Hochsprachen unterschiedlich verwaltet wird. Da diese Frage als sprachunabhängig gekennzeichnet ist, würde ich sagen, dass dieser bestimmte Kommentar / diese Zeile schlecht platziert und nicht zutreffend ist.
LintfordPickle

2
Guter Punkt @JonnoHampson - Während Sie einen gültigen Punkt machen, würde ich argumentieren, dass Sie sich wahrscheinlich überhaupt nicht für Speicherzuweisungsmechanismen interessieren, wenn Sie in einer "Hochsprache" mit einem GC arbeiten - und das auch nicht Kümmere dich sogar darum, was der Stapel und der Haufen sind.
Tom Leys

194

Zur Verdeutlichung hat diese Antwort falsche Informationen ( Thomas hat seine Antwort nach Kommentaren korrigiert , cool :)). Bei anderen Antworten wird nur vermieden, zu erklären, was statische Zuordnung bedeutet. Daher werde ich die drei Hauptformen der Zuordnung erläutern und wie sie sich normalerweise auf das Heap-, Stack- und Datensegment unten beziehen. Ich werde auch einige Beispiele in C / C ++ und Python zeigen, um den Leuten das Verständnis zu erleichtern.

"Statische" (statisch zugewiesene) AKA-Variablen werden dem Stapel nicht zugewiesen. Gehen Sie nicht davon aus - viele Leute tun dies nur, weil "statisch" sehr nach "Stapel" klingt. Sie existieren tatsächlich weder im Stapel noch im Haufen. Sie sind Teil des sogenannten Datensegments .

Im Allgemeinen ist es jedoch besser, " Umfang " und " Lebensdauer " als "Stapel" und "Heap" zu berücksichtigen .

Der Bereich bezieht sich darauf, welche Teile des Codes auf eine Variable zugreifen können. Im Allgemeinen denken wir an den lokalen Bereich (auf den nur mit der aktuellen Funktion zugegriffen werden kann) im Vergleich zum globalen Bereich (auf den überall zugegriffen werden kann), obwohl der Bereich viel komplexer werden kann.

Die Lebensdauer bezieht sich darauf, wann eine Variable während der Programmausführung zugewiesen und freigegeben wird. Normalerweise denken wir an eine statische Zuordnung (Variable bleibt während der gesamten Dauer des Programms erhalten, wodurch sie zum Speichern derselben Informationen über mehrere Funktionsaufrufe hinweg nützlich ist) im Vergleich zur automatischen Zuordnung (Variable bleibt nur während eines einzelnen Aufrufs einer Funktion bestehen, was sie nützlich macht Speichern von Informationen, die nur während Ihrer Funktion verwendet werden und nach Abschluss verworfen werden können, im Vergleich zur dynamischen Zuordnung (Variablen, deren Dauer zur Laufzeit definiert wird, anstelle der Kompilierungszeit wie statisch oder automatisch).

Obwohl die meisten Compiler und Interpreter dieses Verhalten in Bezug auf die Verwendung von Stacks, Heaps usw. ähnlich implementieren, kann ein Compiler manchmal gegen diese Konventionen verstoßen, wenn er dies wünscht, solange das Verhalten korrekt ist. Beispielsweise kann aufgrund der Optimierung eine lokale Variable nur in einem Register vorhanden sein oder vollständig entfernt werden, obwohl die meisten lokalen Variablen im Stapel vorhanden sind. Wie bereits in einigen Kommentaren erwähnt, können Sie einen Compiler implementieren, der nicht einmal einen Stack oder einen Heap verwendet, sondern einige andere Speichermechanismen (selten ausgeführt, da Stacks und Heaps dafür hervorragend geeignet sind).

Ich werde einen einfachen kommentierten C-Code bereitstellen, um all dies zu veranschaulichen. Der beste Weg zu lernen ist, ein Programm unter einem Debugger auszuführen und das Verhalten zu beobachten. Wenn Sie lieber Python lesen möchten, fahren Sie mit dem Ende der Antwort fort :)

// Statically allocated in the data segment when the program/DLL is first loaded
// Deallocated when the program/DLL exits
// scope - can be accessed from anywhere in the code
int someGlobalVariable;

// Statically allocated in the data segment when the program is first loaded
// Deallocated when the program/DLL exits
// scope - can be accessed from anywhere in this particular code file
static int someStaticVariable;

// "someArgument" is allocated on the stack each time MyFunction is called
// "someArgument" is deallocated when MyFunction returns
// scope - can be accessed only within MyFunction()
void MyFunction(int someArgument) {

    // Statically allocated in the data segment when the program is first loaded
    // Deallocated when the program/DLL exits
    // scope - can be accessed only within MyFunction()
    static int someLocalStaticVariable;

    // Allocated on the stack each time MyFunction is called
    // Deallocated when MyFunction returns
    // scope - can be accessed only within MyFunction()
    int someLocalVariable;

    // A *pointer* is allocated on the stack each time MyFunction is called
    // This pointer is deallocated when MyFunction returns
    // scope - the pointer can be accessed only within MyFunction()
    int* someDynamicVariable;

    // This line causes space for an integer to be allocated in the heap
    // when this line is executed. Note this is not at the beginning of
    // the call to MyFunction(), like the automatic variables
    // scope - only code within MyFunction() can access this space
    // *through this particular variable*.
    // However, if you pass the address somewhere else, that code
    // can access it too
    someDynamicVariable = new int;


    // This line deallocates the space for the integer in the heap.
    // If we did not write it, the memory would be "leaked".
    // Note a fundamental difference between the stack and heap
    // the heap must be managed. The stack is managed for us.
    delete someDynamicVariable;

    // In other cases, instead of deallocating this heap space you
    // might store the address somewhere more permanent to use later.
    // Some languages even take care of deallocation for you... but
    // always it needs to be taken care of at runtime by some mechanism.

    // When the function returns, someArgument, someLocalVariable
    // and the pointer someDynamicVariable are deallocated.
    // The space pointed to by someDynamicVariable was already
    // deallocated prior to returning.
    return;
}

// Note that someGlobalVariable, someStaticVariable and
// someLocalStaticVariable continue to exist, and are not
// deallocated until the program exits.

Ein besonders ergreifendes Beispiel dafür, warum es wichtig ist, zwischen Lebensdauer und Gültigkeitsbereich zu unterscheiden, ist, dass eine Variable einen lokalen Gültigkeitsbereich, aber eine statische Lebensdauer haben kann - zum Beispiel "someLocalStaticVariable" im obigen Codebeispiel. Solche Variablen können unsere allgemeinen, aber informellen Namensgewohnheiten sehr verwirrend machen. Wenn wir beispielsweise " lokal " sagen, meinen wir normalerweise " automatisch zugewiesene Variable mit lokalem Gültigkeitsbereich " und wenn wir global sagen, meinen wir normalerweise " statisch zugewiesene Variable mit globalem Gültigkeitsbereich ". Leider sagen viele Leute, wenn es um Dinge wie " statisch zugewiesene Variablen mit Dateibereich " geht, nur ... " huh ??? ".

Einige der Syntaxoptionen in C / C ++ verschärfen dieses Problem - zum Beispiel denken viele Leute, dass globale Variablen aufgrund der unten gezeigten Syntax nicht "statisch" sind.

int var1; // Has global scope and static allocation
static int var2; // Has file scope and static allocation

int main() {return 0;}

Beachten Sie, dass das Einfügen des Schlüsselworts "static" in die obige Deklaration verhindert, dass var2 einen globalen Gültigkeitsbereich hat. Trotzdem hat die globale var1 eine statische Zuordnung. Das ist nicht intuitiv! Aus diesem Grund versuche ich, bei der Beschreibung des Bereichs niemals das Wort "statisch" zu verwenden und stattdessen etwas wie "Datei" oder "Dateibegrenzter" Bereich zu sagen. Viele Benutzer verwenden jedoch den Ausdruck "statisch" oder "statischer Bereich", um eine Variable zu beschreiben, auf die nur über eine Codedatei zugegriffen werden kann. Im Kontext der Lebensdauer bedeutet "statisch" immer, dass die Variable beim Programmstart zugewiesen und beim Beenden des Programms freigegeben wird.

Einige Leute halten diese Konzepte für C / C ++ -spezifisch. Sie sind nicht. Das folgende Python-Beispiel zeigt beispielsweise alle drei Arten der Zuordnung (es gibt einige subtile Unterschiede bei interpretierten Sprachen, auf die ich hier nicht näher eingehen werde).

from datetime import datetime

class Animal:
    _FavoriteFood = 'Undefined' # _FavoriteFood is statically allocated

    def PetAnimal(self):
        curTime = datetime.time(datetime.now()) # curTime is automatically allocatedion
        print("Thank you for petting me. But it's " + str(curTime) + ", you should feed me. My favorite food is " + self._FavoriteFood)

class Cat(Animal):
    _FavoriteFood = 'tuna' # Note since we override, Cat class has its own statically allocated _FavoriteFood variable, different from Animal's

class Dog(Animal):
    _FavoriteFood = 'steak' # Likewise, the Dog class gets its own static variable. Important to note - this one static variable is shared among all instances of Dog, hence it is not dynamic!


if __name__ == "__main__":
    whiskers = Cat() # Dynamically allocated
    fido = Dog() # Dynamically allocated
    rinTinTin = Dog() # Dynamically allocated

    whiskers.PetAnimal()
    fido.PetAnimal()
    rinTinTin.PetAnimal()

    Dog._FavoriteFood = 'milkbones'
    whiskers.PetAnimal()
    fido.PetAnimal()
    rinTinTin.PetAnimal()

# Output is:
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is tuna
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is steak
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is steak
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is tuna
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is milkbones
# Thank you for petting me. But it's 13:05:02.256000, you should feed me. My favorite food is milkbones

Ich würde auf eine statische Variable verweisen, die innerhalb einer Funktion als nur lokal zugänglich deklariert ist , aber im Allgemeinen nicht den Begriff "Gültigkeitsbereich" verwenden. Es kann auch erwähnenswert sein, dass der eine Stapel- / Heap-Aspekt, mit dem Sprachen im Wesentlichen keine Flexibilität aufweisen: Eine Sprache, die den Ausführungskontext auf einem Stapel speichert, kann nicht denselben Stapel verwenden, um Dinge zu speichern, die die Kontexte überleben müssen, in denen sie erstellt wurden . Einige Sprachen wie PostScripthaben mehrere Stapel, aber einen "Haufen", der sich eher wie ein Stapel verhält.
Supercat

@supercat Das alles macht Sinn. Ich definierte den Bereich als "Welche Teile des Codes können auf eine Variable zugreifen " (und denke, dies ist die Standarddefinition), daher denke ich, dass wir uns einig sind :)
Davec

Ich würde den "Umfang" einer Variablen als zeitlich und räumlich begrenzt betrachten. Eine Variable im Klassenobjektbereich muss ihren Wert behalten, solange das Objekt vorhanden ist. Eine Variable innerhalb eines Ausführungskontextbereichs muss ihren Wert behalten, solange die Ausführung in diesem Kontext verbleibt. Eine statische Variablendeklaration erstellt einen Bezeichner, dessen Gültigkeitsbereich an den aktuellen Block gebunden ist, der an eine Variable angehängt ist, deren Gültigkeitsbereich unbegrenzt ist.
Supercat

@supercat Aus diesem Grund verwende ich das Wort Lebensdauer. So bezeichne ich das, was Sie Zeitbereich nennen. Es reduziert die Notwendigkeit, das Wort "Umfang" mit so vielen Bedeutungen zu überladen. Soweit ich das beurteilen kann, scheint es jedoch keinen vollständigen Konsens über genaue Definitionen zu geben, selbst unter kanonischen Quellen. Meine Terminologie basiert teilweise auf K & R und teilweise auf der vorherrschenden Verwendung in der ersten CS-Abteilung, in der ich studiert / unterrichtet habe. Immer gut, eine andere informierte Ansicht zu hören.
Davec

1
Sie machen wohl Witze. Können Sie wirklich eine statische Variable innerhalb einer Funktion definieren?
Zaeem Sattar

168

Andere haben die breiten Striche ziemlich gut beantwortet, deshalb werde ich ein paar Details einbringen.

  1. Stapel und Haufen müssen nicht singulär sein. Eine häufige Situation, in der Sie mehr als einen Stapel haben, ist, wenn Sie mehr als einen Thread in einem Prozess haben. In diesem Fall hat jeder Thread seinen eigenen Stapel. Sie können auch mehr als einen Heap haben. Einige DLL-Konfigurationen können beispielsweise dazu führen, dass unterschiedliche DLLs von unterschiedlichen Heaps zugewiesen werden. Daher ist es im Allgemeinen eine schlechte Idee, den von einer anderen Bibliothek zugewiesenen Speicher freizugeben.

  2. In C können Sie den Vorteil einer Zuweisung mit variabler Länge durch die Verwendung von Zuweisung erhalten , die auf dem Stapel zugewiesen wird, im Gegensatz zu Zuweisung, die auf dem Heap zugewiesen wird. Dieser Speicher überlebt Ihre return-Anweisung nicht, ist jedoch für einen Arbeitspuffer nützlich.

  3. Es ist nicht kostenlos, unter Windows einen riesigen temporären Puffer zu erstellen, von dem Sie nicht viel verwenden. Dies liegt daran, dass der Compiler eine Stapelprüfschleife generiert, die bei jeder Eingabe Ihrer Funktion aufgerufen wird, um sicherzustellen, dass der Stapel vorhanden ist (da Windows eine einzelne Schutzseite am Ende Ihres Stapels verwendet, um zu erkennen, wann der Stapel vergrößert werden muss. Wenn Sie mehr als eine Seite am Ende des Stapels auf den Speicher zugreifen, stürzen Sie ab. Beispiel:

void myfunction()
{
   char big[10000000];
   // Do something that only uses for first 1K of big 99% of the time.
}

Re "im Gegensatz zu alloc": Meinst du "im Gegensatz zu malloc"?
Peter Mortensen

Wie tragbar ist alloca?
Peter Mortensen

@PeterMortensen es ist nicht POSIX, Portabilität nicht garantiert.
Don Neufeld

135

Andere haben Ihre Frage direkt beantwortet, aber wenn Sie versuchen, den Stapel und den Heap zu verstehen, halte ich es für hilfreich, das Speicherlayout eines herkömmlichen UNIX-Prozesses (ohne Threads und mmap()allokierte Allokatoren) zu berücksichtigen . Die Memory Management Glossar Web - Seite hat ein Diagramm dieses Speicherlayout.

Der Stapel und der Heap befinden sich traditionell an entgegengesetzten Enden des virtuellen Adressraums des Prozesses. Der Stapel wächst beim Zugriff automatisch bis zu einer vom Kernel festgelegten Größe (mit der angepasst werden kann setrlimit(RLIMIT_STACK, ...)). Der Heap wächst, wenn der Speicherzuweiser den Systemaufruf brk()oder aufruft sbrk()und mehr Seiten physischen Speichers dem virtuellen Adressraum des Prozesses zuordnet.

In Systemen ohne virtuellen Speicher, wie z. B. einigen eingebetteten Systemen, gilt häufig das gleiche Grundlayout, außer dass der Stapel und der Heap eine feste Größe haben. In anderen eingebetteten Systemen (wie z. B. solchen, die auf Microchip PIC-Mikrocontrollern basieren) ist der Programmstapel jedoch ein separater Speicherblock, der nicht durch Datenbewegungsanweisungen adressiert werden kann und nur durch Programmflussanweisungen (Aufruf, Rückkehr usw.). Andere Architekturen, wie z. B. Intel Itanium-Prozessoren, haben mehrere Stapel . In diesem Sinne ist der Stack ein Element der CPU-Architektur.


117

Was ist ein Stapel?

Ein Stapel ist ein Stapel von Objekten, normalerweise einer, der ordentlich angeordnet ist.

Geben Sie hier die Bildbeschreibung ein

Stapel in Computerarchitekturen sind Speicherbereiche, in denen Daten auf Last-In-First-Out-Weise hinzugefügt oder entfernt werden.
In einer Multithread-Anwendung hat jeder Thread seinen eigenen Stapel.

Was ist ein Haufen?

Ein Haufen ist eine unordentliche Sammlung von Dingen, die sich willkürlich stapeln.

Geben Sie hier die Bildbeschreibung ein

In Computerarchitekturen ist der Heap ein Bereich mit dynamisch zugewiesenem Speicher, der automatisch vom Betriebssystem oder der Speichermanagerbibliothek verwaltet wird.
Der Speicher auf dem Heap wird während der Programmausführung regelmäßig zugewiesen, freigegeben und in der Größe geändert. Dies kann zu einem Problem führen, das als Fragmentierung bezeichnet wird.
Fragmentierung tritt auf, wenn Speicherobjekte mit kleinen Zwischenräumen zugewiesen werden, die zu klein sind, um zusätzliche Speicherobjekte aufzunehmen.
Das Nettoergebnis ist ein Prozentsatz des Heapspeichers, der nicht für weitere Speicherzuweisungen verwendet werden kann.

Beide zusammen

In einer Multithread-Anwendung hat jeder Thread seinen eigenen Stapel. Alle verschiedenen Threads teilen sich jedoch den Heap.
Da sich die verschiedenen Threads den Heap in einer Multithread-Anwendung gemeinsam nutzen, bedeutet dies auch, dass eine gewisse Koordination zwischen den Threads erforderlich ist, damit sie nicht versuchen, auf dieselben Speicherelemente im Heap bei zuzugreifen und diese zu bearbeiten die selbe Zeit.

Was ist schneller - der Stapel oder der Haufen? Und warum?

Der Stapel ist viel schneller als der Haufen.
Dies liegt an der Art und Weise, wie Speicher auf dem Stapel zugewiesen wird.
Das Zuweisen von Speicher auf dem Stapel ist so einfach wie das Bewegen des Stapelzeigers nach oben.

Für Programmieranfänger ist es wahrscheinlich eine gute Idee, den Stack zu verwenden, da dies einfacher ist.
Da der Stapel klein ist, sollten Sie ihn verwenden, wenn Sie genau wissen, wie viel Speicher Sie für Ihre Daten benötigen, oder wenn Sie wissen, dass Ihre Daten sehr klein sind.
Es ist besser, den Heap zu verwenden, wenn Sie wissen, dass Sie viel Speicher für Ihre Daten benötigen oder einfach nicht sicher sind, wie viel Speicher Sie benötigen (wie bei einem dynamischen Array).

Java-Speichermodell

Geben Sie hier die Bildbeschreibung ein

Der Stapel ist der Speicherbereich, in dem lokale Variablen (einschließlich Methodenparameter) gespeichert sind. Bei Objektvariablen handelt es sich lediglich um Verweise (Zeiger) auf die tatsächlichen Objekte auf dem Heap.
Jedes Mal, wenn ein Objekt instanziiert wird, wird ein Teil des Heapspeichers beiseite gelegt, um die Daten (den Status) dieses Objekts zu speichern. Da Objekte andere Objekte enthalten können, können einige dieser Daten tatsächlich Verweise auf diese verschachtelten Objekte enthalten.


115

Der Stapel ist ein Teil des Speichers, der über mehrere wichtige Anweisungen in Assemblersprache bearbeitet werden kann, z. B. 'pop' (Entfernen und Zurückgeben eines Werts vom Stapel) und 'push' (Verschieben eines Werts auf den Stapel), aber auch Aufrufen ( Rufen Sie eine Unterroutine auf - dies drückt die Adresse, um zum Stapel zurückzukehren) und kehren Sie zurück (Rückkehr von einer Unterroutine - dies entfernt die Adresse vom Stapel und springt dorthin). Dies ist der Speicherbereich unterhalb des Stapelzeigerregisters, der nach Bedarf eingestellt werden kann. Der Stapel wird auch zum Übergeben von Argumenten an Unterroutinen und zum Beibehalten der Werte in Registern vor dem Aufrufen von Unterroutinen verwendet.

Der Heap ist ein Teil des Speichers, der vom Betriebssystem an eine Anwendung übergeben wird, normalerweise über einen Systemaufruf wie malloc. Unter modernen Betriebssystemen besteht dieser Speicher aus einer Reihe von Seiten, auf die nur der aufrufende Prozess Zugriff hat.

Die Größe des Stapels wird zur Laufzeit festgelegt und wächst im Allgemeinen nach dem Start des Programms nicht mehr. In einem C-Programm muss der Stapel groß genug sein, um jede in jeder Funktion deklarierte Variable aufzunehmen. Der Heap wächst nach Bedarf dynamisch, aber das Betriebssystem führt letztendlich den Aufruf durch (häufig wird der Heap um mehr als den von malloc angeforderten Wert vergrößert, sodass zumindest einige zukünftige Mallocs nicht zum Kernel zurückkehren müssen Holen Sie sich mehr Speicher. Dieses Verhalten ist oft anpassbar.

Da Sie den Stapel vor dem Starten des Programms zugewiesen haben, müssen Sie nie mallocieren, bevor Sie den Stapel verwenden können. Dies ist dort also ein kleiner Vorteil. In der Praxis ist es sehr schwer vorherzusagen, was in modernen Betriebssystemen mit virtuellen Speichersubsystemen schnell und was langsam sein wird, da die Art und Weise, wie die Seiten implementiert und wo sie gespeichert werden, ein Implementierungsdetail ist.


2
Erwähnenswert ist auch, dass Intel die Stapelzugriffe stark optimiert, insbesondere Dinge wie die Vorhersage, wo Sie von einer Funktion zurückkehren.
Tom Leys

113

Ich denke, viele andere Leute haben Ihnen in dieser Angelegenheit größtenteils richtige Antworten gegeben.

Ein Detail, das jedoch übersehen wurde, ist, dass der "Haufen" tatsächlich wahrscheinlich als "freier Laden" bezeichnet werden sollte. Der Grund für diese Unterscheidung ist, dass der ursprüngliche freie Speicher mit einer Datenstruktur implementiert wurde, die als "Binomialheap" bekannt ist. Aus diesem Grund war die Zuweisung aus frühen Implementierungen von malloc () / free () die Zuweisung von einem Haufen. In der heutigen Zeit werden die meisten freien Speicher jedoch mit sehr ausgefeilten Datenstrukturen implementiert, die keine Binomialhaufen sind.


8
Ein weiterer Trottel - die meisten Antworten (leichtfertig) implizieren, dass die Sprache die Verwendung eines "Stapels" erfordert C. Dies ist ein weit verbreitetes Missverständnis, obwohl es das (bei weitem) dominierende Paradigma für die Implementierung von C99 6.2.4 automatic storage duration objects(Variablen) ist. In der Tat bedeutet das Wort „Stack“ erscheint nicht einmal in der C99Sprache Standard: open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf
Johne

[@Heath] Ich habe einen kleinen Kommentar zu Ihrer Antwort. Schauen Sie sich die akzeptierte Antwort auf diese Frage an . Es heißt, dass der freie Laden höchstwahrscheinlich der gleiche ist wie der Haufen , obwohl dies nicht unbedingt der Fall ist.
OmarOthman

91

Mit dem Stack können Sie einige interessante Dinge tun. Zum Beispiel haben Sie Funktionen wie alloca (vorausgesetzt, Sie können die zahlreichen Warnungen bezüglich seiner Verwendung überwinden ), eine Form von malloc, die speziell den Stapel und nicht den Heap als Speicher verwendet.

Trotzdem sind stapelbasierte Speicherfehler einige der schlimmsten, die ich je erlebt habe. Wenn Sie Heapspeicher verwenden und die Grenzen Ihres zugewiesenen Blocks überschreiten, besteht eine gute Chance, dass ein Segmentfehler ausgelöst wird. (Nicht 100%: Ihr Block kann nebenbei mit einem anderen Block verknüpft sein, den Sie zuvor zugewiesen haben.) Da jedoch auf dem Stapel erstellte Variablen immer zusammenhängend sind, kann das Schreiben außerhalb der Grenzen den Wert einer anderen Variablen ändern. Ich habe gelernt, dass mein Programm, wenn ich das Gefühl habe, die Gesetze der Logik nicht mehr zu befolgen, wahrscheinlich ein Pufferüberlauf ist.


Wie tragbar ist alloca? Funktioniert es beispielsweise unter Windows? Ist es nur für Unix-ähnliche Betriebssysteme?
Peter Mortensen

89

Auf dem Stapel werden einfach lokale Variablen erstellt. Außerdem werden jedes Mal, wenn Sie eine Unterroutine aufrufen, der Programmzähler (Zeiger auf die nächste Maschinenanweisung) und alle wichtigen Register und manchmal die Parameter auf den Stapel verschoben. Dann werden alle lokalen Variablen innerhalb des Unterprogramms auf den Stapel verschoben (und von dort aus verwendet). Wenn die Unterroutine beendet ist, wird das Zeug wieder vom Stapel genommen. Die PC- und Registerdaten werden abgerufen und wieder dort abgelegt, wo sie sich gerade befinden, sodass Ihr Programm seinen fröhlichen Weg fortsetzen kann.

Der Heap ist der Bereich, aus dem dynamische Speicherzuordnungen für den Speicher erstellt werden (explizite "neue" oder "zugewiesene" Aufrufe). Es handelt sich um eine spezielle Datenstruktur, die Speicherblöcke unterschiedlicher Größe und deren Zuordnungsstatus verfolgen kann.

In "klassischen" Systemen wurde RAM so angelegt, dass der Stapelzeiger am unteren Rand des Speichers begann, der Heap-Zeiger am oberen Rand begann und sie aufeinander zuwuchsen. Wenn sie sich überschneiden, haben Sie keinen RAM mehr. Dies funktioniert jedoch nicht mit modernen Multithread-Betriebssystemen. Jeder Thread muss einen eigenen Stapel haben, und diese können dynamisch erstellt werden.


[@TED] Warum hast du gesagt "manchmal werden die Parameter auf den Stapel geschoben"? Was ich weiß ist, dass sie es immer sind. Könnten Sie bitte näher darauf eingehen?
OmarOthman

1
@OmarOthman - Ich sage das, weil es ganz dem Verfasser Ihres Compilers / Interpreters überlassen bleibt, was passiert, wenn eine Unterroutine aufgerufen wird. Das klassische Fortran-Verhalten besteht darin, überhaupt keinen Stapel zu verwenden. Einige Sprachen unterstützen exotische Dinge wie Pass-by-Name, was praktisch eine Textersetzung ist.
TED

83

Von WikiAnwser.

Stapel

Wenn eine Funktion oder eine Methode eine andere Funktion aufruft, die wiederum eine andere Funktion usw. aufruft, bleibt die Ausführung all dieser Funktionen ausgesetzt, bis die allerletzte Funktion ihren Wert zurückgibt.

Diese Kette angehaltener Funktionsaufrufe ist der Stapel, da Elemente im Stapel (Funktionsaufrufe) voneinander abhängen.

Der Stapel ist wichtig bei der Ausnahmebehandlung und Thread-Ausführung.

Haufen

Der Heap ist einfach der Speicher, der von Programmen zum Speichern von Variablen verwendet wird. Elemente des Heaps (Variablen) haben keine Abhängigkeiten voneinander und können jederzeit nach dem Zufallsprinzip aufgerufen werden.


"Ich mag die akzeptierte Antwort besser, da sie noch niedriger ist." Das ist eine schlechte Sache, keine gute Sache.
Leichtigkeitsrennen im Orbit

54

Stapel

  • Sehr schneller Zugang
  • Sie müssen Variablen nicht explizit freigeben
  • Der Speicherplatz wird effizient von der CPU verwaltet, der Speicher wird nicht fragmentiert
  • Nur lokale Variablen
  • Begrenzung der Stapelgröße (OS-abhängig)
  • Die Größe von Variablen kann nicht geändert werden

Haufen

  • Auf Variablen kann global zugegriffen werden
  • Keine Begrenzung der Speichergröße
  • (Relativ) langsamerer Zugriff
  • Ohne garantierte effiziente Speicherplatznutzung kann der Speicher im Laufe der Zeit fragmentiert werden, wenn Speicherblöcke zugewiesen und dann freigegeben werden
  • Sie müssen den Speicher verwalten (Sie sind für die Zuweisung und Freigabe von Variablen verantwortlich).
  • Die Größe von Variablen kann mit realloc () geändert werden.

50

OK, einfach und in kurzen Worten, sie bedeuten bestellt und nicht bestellt ...!

Stapel : Bei Stapelgegenständen überlagern sich die Dinge, was bedeutet, dass die Verarbeitung schneller und effizienter ist! ...

Es gibt also immer einen Index, der auf das jeweilige Element verweist. Außerdem wird die Verarbeitung schneller, es besteht auch eine Beziehung zwischen den Elementen! ...

Haufen : Keine Reihenfolge, die Verarbeitung wird langsamer und die Werte werden ohne bestimmte Reihenfolge oder Index durcheinander gebracht. Es gibt zufällige und es gibt keine Beziehung zwischen ihnen. Daher können Ausführungs- und Verwendungszeit variieren.

Ich erstelle auch das Bild unten, um zu zeigen, wie sie aussehen können:

Geben Sie hier die Bildbeschreibung ein


49

Zusamenfassend

Ein Stapel wird für die statische Speicherzuweisung und ein Heap für die dynamische Speicherzuweisung verwendet, die beide im RAM des Computers gespeichert sind.


Im Detail

Der Stapel

Der Stack ist eine "LIFO" -Datenstruktur (last in, first out), die von der CPU sehr genau verwaltet und optimiert wird. Jedes Mal, wenn eine Funktion eine neue Variable deklariert, wird sie auf den Stapel "geschoben". Jedes Mal, wenn eine Funktion beendet wird, werden alle von dieser Funktion auf den Stapel geschobenen Variablen freigegeben (dh sie werden gelöscht). Sobald eine Stapelvariable freigegeben ist, wird dieser Speicherbereich für andere Stapelvariablen verfügbar.

Der Vorteil der Verwendung des Stapels zum Speichern von Variablen besteht darin, dass der Speicher für Sie verwaltet wird. Sie müssen den Speicher nicht manuell zuweisen oder freigeben, wenn Sie ihn nicht mehr benötigen. Da die CPU den Stapelspeicher so effizient organisiert, ist das Lesen und Schreiben von Stapelvariablen sehr schnell.

Mehr finden Sie hier .


Der Haufen

Der Heap ist ein Bereich des Arbeitsspeichers Ihres Computers, der nicht automatisch für Sie verwaltet wird und von der CPU nicht so streng verwaltet wird. Es ist ein frei schwebender Speicherbereich (und größer). Um Speicher auf dem Heap zuzuweisen, müssen Sie malloc () oder calloc () verwenden, die integrierte C-Funktionen sind. Sobald Sie Speicher auf dem Heap zugewiesen haben, sind Sie dafür verantwortlich, free () zu verwenden, um die Zuweisung dieses Speichers aufzuheben, sobald Sie ihn nicht mehr benötigen.

Wenn Sie dies nicht tun, weist Ihr Programm einen sogenannten Speicherverlust auf. Das heißt, der Speicher auf dem Heap wird weiterhin reserviert (und steht anderen Prozessen nicht zur Verfügung). Wie wir im Abschnitt zum Debuggen sehen werden, gibt es ein Tool namens Valgrind , mit dem Sie Speicherlecks erkennen können.

Im Gegensatz zum Stapel unterliegt der Heap keinen Größenbeschränkungen für die variable Größe (abgesehen von den offensichtlichen physischen Einschränkungen Ihres Computers). Der Heap-Speicher ist etwas langsamer zum Lesen und Schreiben, da Zeiger verwendet werden müssen, um auf den Speicher des Heaps zuzugreifen. Wir werden in Kürze über Hinweise sprechen.

Im Gegensatz zum Stapel können Variablen, die auf dem Heap erstellt wurden, von jeder Funktion an einer beliebigen Stelle in Ihrem Programm aufgerufen werden. Heap-Variablen sind im Wesentlichen global.

Mehr finden Sie hier .


Auf dem Stapel zugewiesene Variablen werden direkt im Speicher gespeichert, und der Zugriff auf diesen Speicher ist sehr schnell, und seine Zuordnung wird beim Kompilieren des Programms behandelt. Wenn eine Funktion oder eine Methode eine andere Funktion aufruft, die wiederum eine andere Funktion usw. aufruft, bleibt die Ausführung all dieser Funktionen ausgesetzt, bis die allerletzte Funktion ihren Wert zurückgibt. Der Stapel wird immer in einer LIFO-Reihenfolge reserviert. Der zuletzt reservierte Block ist immer der nächste freizugebende Block. Dies macht es wirklich einfach, den Stapel im Auge zu behalten. Das Lösen eines Blocks vom Stapel ist nichts anderes als das Anpassen eines Zeigers.

Auf dem Heap zugewiesenen Variablen wird zur Laufzeit der Speicher zugewiesen, und der Zugriff auf diesen Speicher ist etwas langsamer, die Größe des Heapspeichers ist jedoch nur durch die Größe des virtuellen Speichers begrenzt. Elemente des Heaps haben keine Abhängigkeiten voneinander und können jederzeit nach dem Zufallsprinzip aufgerufen werden. Sie können einen Block jederzeit zuweisen und jederzeit freigeben. Dies macht es viel komplexer zu verfolgen, welche Teile des Heaps zu einem bestimmten Zeitpunkt zugewiesen oder frei sind.

Geben Sie hier die Bildbeschreibung ein

Sie können den Stapel verwenden, wenn Sie genau wissen, wie viele Daten Sie vor der Kompilierungszeit zuweisen müssen, und er nicht zu groß ist. Sie können den Heap verwenden, wenn Sie nicht genau wissen, wie viele Daten Sie zur Laufzeit benötigen oder wenn Sie viele Daten zuweisen müssen.

In einer Situation mit mehreren Threads hat jeder Thread seinen eigenen, völlig unabhängigen Stapel, aber sie teilen sich den Heap. Der Stack ist threadspezifisch und der Heap ist anwendungsspezifisch. Der Stapel ist wichtig bei der Ausnahmebehandlung und Thread-Ausführung.

Jeder Thread erhält einen Stapel, während es normalerweise nur einen Heap für die Anwendung gibt (obwohl es nicht ungewöhnlich ist, mehrere Heaps für verschiedene Zuordnungstypen zu haben).

Geben Sie hier die Bildbeschreibung ein

Wenn die Anwendung zur Laufzeit mehr Heap benötigt, kann sie Speicher aus dem freien Speicher zuweisen, und wenn der Stapel Speicher benötigt, kann sie Speicher aus dem freien Speicher zuweisen, der der Anwendung zugewiesen ist.

Noch mehr Details werden hier und hier gegeben .


Kommen Sie nun zu den Antworten Ihrer Frage .

Inwieweit werden sie vom Betriebssystem oder der Sprachlaufzeit gesteuert?

Das Betriebssystem weist den Stapel jedem Thread auf Systemebene zu, wenn der Thread erstellt wird. In der Regel wird das Betriebssystem von der Sprachlaufzeit aufgerufen, um den Heap für die Anwendung zuzuweisen.

Mehr finden Sie hier .

Was ist ihr Umfang?

Bereits oben angegeben.

"Sie können den Stapel verwenden, wenn Sie genau wissen, wie viele Daten Sie vor der Kompilierungszeit zuweisen müssen, und er nicht zu groß ist. Sie können den Heap verwenden, wenn Sie nicht genau wissen, wie viele Daten Sie zur Laufzeit benötigen oder wenn Sie müssen viele Daten zuweisen. "

Mehr finden Sie hier .

Was bestimmt die Größe jedes einzelnen von ihnen?

Die Größe des Stapels wird vom Betriebssystem festgelegt, wenn ein Thread erstellt wird. Die Größe des Heapspeichers wird beim Start der Anwendung festgelegt, kann jedoch bei Bedarf an Speicherplatz zunehmen (der Allokator fordert mehr Speicher vom Betriebssystem an).

Was macht einen schneller?

Die Stapelzuweisung ist viel schneller, da nur der Stapelzeiger bewegt wird. Wenn Sie Speicherpools verwenden, können Sie eine vergleichbare Leistung bei der Heap-Zuweisung erzielen. Dies ist jedoch mit einer geringfügig zusätzlichen Komplexität und eigenen Kopfschmerzen verbunden.

Außerdem ist Stack vs. Heap nicht nur eine Leistungsüberlegung. Außerdem erfahren Sie viel über die erwartete Lebensdauer von Objekten.

Details finden Sie hier .


36

In den 1980er Jahren verbreitete sich UNIX wie ein Hase, und große Unternehmen rollten ihre eigenen. Exxon hatte einen, ebenso wie Dutzende von Markennamen, die der Geschichte verloren gingen. Wie der Speicher angelegt wurde, lag im Ermessen der vielen Implementierer.

Ein typisches C-Programm wurde flach im Speicher angelegt, mit der Möglichkeit, durch Ändern des brk () -Werts zu erhöhen. Typischerweise lag der HEAP knapp unter diesem brk-Wert und eine Erhöhung von brk erhöhte die Menge des verfügbaren Heaps.

Der einzelne STACK war typischerweise ein Bereich unterhalb von HEAP, der ein Speicherbereich war, der bis zum oberen Rand des nächsten festen Speicherblocks nichts Wertvolles enthielt. Dieser nächste Block war oft CODE, der durch Stapeldaten in einem der berühmten Hacks seiner Zeit überschrieben werden konnte.

Ein typischer Speicherblock war BSS (ein Block mit Nullwerten), der im Angebot eines Herstellers versehentlich nicht auf Null gesetzt wurde. Ein anderes war DATA, das initialisierte Werte enthielt, einschließlich Zeichenfolgen und Zahlen. Ein dritter war CODE, der CRT (C-Laufzeit), main, Funktionen und Bibliotheken enthielt.

Das Aufkommen des virtuellen Speichers unter UNIX ändert viele der Einschränkungen. Es gibt keinen objektiven Grund, warum diese Blöcke zusammenhängend oder in der Größe festgelegt oder jetzt auf eine bestimmte Weise bestellt werden müssen. Natürlich gab es vor UNIX Multics, die nicht unter diesen Einschränkungen litten. Hier ist ein Schema, das eines der Speicherlayouts dieser Ära zeigt.

Ein typisches UNIX C-Programmspeicherlayout im Stil der 1980er Jahre



26

Ein paar Cent: Ich denke, es wird gut sein, Speicher grafisch und einfacher zu zeichnen:

Dies ist meine Vision einer Prozessspeicherkonstruktion mit Vereinfachung, um das Verständnis des Geschehens zu erleichtern


Pfeile - zeigen an, wo der Stapel und der Heap wachsen, die Größe des Prozessstapels begrenzt ist, definiert im Betriebssystem, die Größe des Thread-Stacks durch Parameter in der Thread-Erstellungs-API normalerweise. Heap, der normalerweise die maximale Größe des virtuellen Speichers pro Prozess begrenzt, z. B. für 32 Bit 2-4 GB.

So einfach: Der Prozessheap ist allgemein für den Prozess und alle darin enthaltenen Threads und wird für die Speicherzuweisung verwendet, wie dies bei malloc () üblich ist .

Der Stapel ist ein schneller Speicher zum Speichern von Funktionsrückgabezeigern und -variablen im allgemeinen Fall, die als Parameter im Funktionsaufruf und als lokale Funktionsvariablen verarbeitet werden.


23

Da einige Antworten nicht ausgewählt wurden, werde ich meine Milbe beisteuern.

Überraschenderweise hat niemand erwähnt, dass mehrere (dh nicht mit der Anzahl der laufenden Threads auf Betriebssystemebene zusammenhängende) Aufrufstapel nicht nur in exotischen Sprachen (PostScript) oder Plattformen (Intel Itanium), sondern auch in Fasern , grünen Threads, zu finden sind und einige Implementierungen von Coroutinen .

Fasern, grüne Fäden und Coroutinen sind in vielerlei Hinsicht ähnlich, was zu viel Verwirrung führt. Der Unterschied zwischen Fasern und grünen Fäden besteht darin, dass die ersteren kooperatives Multitasking verwenden, während die letzteren entweder kooperatives oder präventives (oder sogar beides) aufweisen können. Zur Unterscheidung zwischen Fasern und Coroutinen siehe hier .

In jedem Fall besteht der Zweck von Fasern, grünen Threads und Coroutinen darin, dass mehrere Funktionen gleichzeitig, jedoch nicht parallel (siehe diese SO-Frage zur Unterscheidung) innerhalb eines einzelnen Threads auf Betriebssystemebene ausgeführt werden und die Steuerung voneinander hin und her übertragen wird auf organisierte Weise.

Wenn Sie Fasern, grüne Fäden oder Coroutinen verwenden, haben Sie normalerweise einen separaten Stapel pro Funktion. (Technisch gesehen ist nicht nur ein Stapel, sondern ein ganzer Ausführungskontext pro Funktion. Am wichtigsten ist, dass sich die CPU registriert.) Für jeden Thread gibt es so viele Stapel, wie gleichzeitig Funktionen ausgeführt werden, und der Thread wechselt zwischen der Ausführung jeder Funktion gemäß der Logik Ihres Programms. Wenn eine Funktion zu Ende geht, wird ihr Stapel zerstört. Also, die Anzahl und die Lebensdauer der Stacks sind dynamisch und werden nicht durch die Anzahl der OS-Level - Threads bestimmt!

Beachten Sie, dass ich sagte " normalerweise einen separaten Stapel pro Funktion haben". Es gibt sowohl stapelbar als auch stapellos Implementierungen von Couroutinen. Am bemerkenswertesten stackful C ++ Implementierungen sind Boost.Coroutine und Microsoft PPL s‘ async/await. (Die wiederaufnehmbaren Funktionen von C ++ (auch bekannt als " asyncund await"), die in C ++ 17 vorgeschlagen wurden, verwenden wahrscheinlich stapellose Coroutinen.)

Der Vorschlag für Fasern zur C ++ - Standardbibliothek ist in Vorbereitung. Es gibt auch einige Bibliotheken von Drittanbietern . Grüne Fäden sind in Sprachen wie Python und Ruby äußerst beliebt.


19

Ich habe etwas zu teilen, obwohl die wichtigsten Punkte bereits behandelt werden.

Stapel

  • Sehr schneller Zugang.
  • Im RAM gespeichert.
  • Hier werden Funktionsaufrufe zusammen mit den übergebenen lokalen Variablen und Funktionsparametern geladen.
  • Der Speicherplatz wird automatisch freigegeben, wenn das Programm einen Bereich verlässt.
  • Im sequentiellen Speicher gespeichert.

Haufen

  • Langsamer Zugriff im Vergleich zu Stack.
  • Im RAM gespeichert.
  • Hier werden dynamisch erstellte Variablen gespeichert, die später die Freigabe des zugewiesenen Speichers nach der Verwendung erfordern.
  • Wird überall dort gespeichert, wo die Speicherzuweisung erfolgt, und wird immer mit dem Zeiger aufgerufen.

Interessanter Hinweis:

  • Wenn die Funktionsaufrufe auf einem Haufen gespeichert worden wären, hätte dies zu 2 unordentlichen Punkten geführt:
    1. Aufgrund der sequentiellen Speicherung im Stapel ist die Ausführung schneller. Die Speicherung im Heap hätte zu einem enormen Zeitaufwand geführt, wodurch das gesamte Programm langsamer ausgeführt würde.
    2. Wenn Funktionen im Heap gespeichert wären (unordentlicher Speicher, auf den der Zeiger zeigt), hätte es keine Möglichkeit gegeben, zur Aufruferadresse zurückzukehren (die der Stapel aufgrund der sequentiellen Speicherung im Speicher angibt).

1
prägnant und sauber. schön :)
ingconti

13

Beeindruckend! So viele Antworten und ich glaube nicht, dass einer von ihnen es richtig gemacht hat ...

1) Wo und was sind sie (physisch im Speicher eines realen Computers)?

Der Stapel ist ein Speicher, der als höchste Speicheradresse beginnt, die Ihrem Programmabbild zugewiesen ist, und von dort aus an Wert verliert. Es ist für aufgerufene Funktionsparameter und für alle in Funktionen verwendeten temporären Variablen reserviert.

Es gibt zwei Haufen: öffentliche und private.

Der private Heap beginnt an einer 16-Byte-Grenze (für 64-Bit-Programme) oder einer 8-Byte-Grenze (für 32-Bit-Programme) nach dem letzten Codebyte in Ihrem Programm und nimmt von dort aus an Wert zu. Es wird auch als Standardheap bezeichnet.

Wenn der private Heap zu groß wird, überlappt er den Stapelbereich, ebenso wie der Stapel den Heap, wenn er zu groß wird. Da der Stapel an einer höheren Adresse beginnt und sich zu einer niedrigeren Adresse hinunterarbeitet, können Sie den Stapel bei ordnungsgemäßem Hacking so groß machen, dass er den privaten Heap-Bereich überläuft und den Codebereich überlappt. Der Trick besteht dann darin, den Codebereich so weit zu überlappen, dass Sie ihn in den Code einbinden können. Es ist etwas schwierig und Sie riskieren einen Programmabsturz, aber es ist einfach und sehr effektiv.

Der öffentliche Heap befindet sich in seinem eigenen Speicherbereich außerhalb Ihres Programmabbildbereichs. Dieser Speicher wird auf die Festplatte übertragen, wenn die Speicherressourcen knapp werden.

2) Inwieweit werden sie vom Betriebssystem oder der Sprachlaufzeit gesteuert?

Der Stack wird vom Programmierer gesteuert, der private Heap wird vom Betriebssystem verwaltet und der öffentliche Heap wird von niemandem gesteuert, da es sich um einen Betriebssystemdienst handelt. Sie stellen Anforderungen und entweder werden sie gewährt oder abgelehnt.

2b) Was ist ihr Umfang?

Sie sind alle global für das Programm, aber ihre Inhalte können privat, öffentlich oder global sein.

2c) Was bestimmt die Größe von jedem von ihnen?

Die Größe des Stacks und des privaten Heaps wird durch die Laufzeitoptionen Ihres Compilers bestimmt. Der öffentliche Heap wird zur Laufzeit mithilfe eines Größenparameters initialisiert.

2d) Was macht einen schneller?

Sie sind nicht so konzipiert, dass sie schnell sind, sondern so, dass sie nützlich sind. Wie der Programmierer sie verwendet, bestimmt, ob sie "schnell" oder "langsam" sind.

REF:

https://norasandler.com/2019/02/18/Write-a-Compiler-10.html

https://docs.microsoft.com/en-us/windows/desktop/api/heapapi/nf-heapapi-getprocessheap

https://docs.microsoft.com/en-us/windows/desktop/api/heapapi/nf-heapapi-heapcreate


8

Viele Antworten sind als Konzepte korrekt, aber wir müssen beachten, dass die Hardware (dh der Mikroprozessor) einen Stapel benötigt, um Unterprogramme aufrufen zu können (CALL in Assemblersprache ..). (OOP Jungs werden es Methoden nennen )

Auf dem Stack speichern Sie Absenderadressen und Aufruf → Push / Ret → Pop wird direkt in der Hardware verwaltet.

Sie können den Stapel verwenden, um Parameter zu übergeben. Auch wenn er langsamer ist als die Verwendung von Registern (würde ein Mikroprozessor-Guru sagen oder ein gutes BIOS-Buch aus den 1980er Jahren ...).

  • Ohne Stack kann kein Mikroprozessor arbeiten. (Wir können uns kein Programm vorstellen, auch nicht in Assemblersprache, ohne Unterprogramme / Funktionen)
  • Ohne den Haufen kann es. (Ein Assembler-Programm kann ohne funktionieren, da der Heap ein Betriebssystemkonzept ist, als Malloc, dh ein OS / Lib-Aufruf.

Die Stapelnutzung ist schneller als:

  • Ist Hardware und sogar Push / Pop sind sehr effizient.
  • Für malloc muss der Kernelmodus aufgerufen, Lock / Semaphore (oder andere Synchronisationsprimitive) verwendet werden, um Code auszuführen, und einige Strukturen verwaltet werden, die zur Verfolgung der Zuordnung erforderlich sind.

Was ist OPP? Meinen Sie OOP ( objektorientierte_Programmierung )?
Peter Mortensen

Wollen Sie damit sagen, dass dies mallocein Kernel-Aufruf ist?
Peter Mortensen

1) ja, sorry .. OOP ... 2) malloc: Ich schreibe kurz, sorry ... malloc befindet sich im User Space .. kann aber andere Anrufe auslösen .... der Punkt ist, dass die Verwendung von Heap sehr langsam sein kann ...
ingconti

" Viele Antworten sind als Konzepte korrekt, aber wir müssen beachten, dass die Hardware (dh der Mikroprozessor) einen Stapel benötigt, um das Aufrufen von Unterprogrammen (CALL in Assemblersprache) zu ermöglichen. " Sie verwechseln den CPU-Stack (falls es einen in der modernen CPU gab) und die Sprachlaufzeitstacks (einen pro Thread). Wenn Programmierer über einen Stapel sprechen, ist dies der Thread-Ausführungsstapel der Laufzeit, z. B. ein NET-Thread-Stapel. Wir sprechen nicht über den CPU-Stapel.
Minuten

1

Der Stapel ist im Wesentlichen ein leicht zugänglicher Speicher, der seine Elemente einfach als - gut - Stapel verwaltet. Nur Gegenstände, deren Größe im Voraus bekannt ist, können auf den Stapel gelegt werden . Dies ist bei Zahlen, Zeichenfolgen und Booleschen Werten der Fall.

Der Heap ist ein Speicher für Elemente, für die Sie die genaue Größe und Struktur nicht vorbestimmen können . Da Objekte und Arrays zur Laufzeit mutiert werden und sich ändern können, müssen sie in den Heap verschoben werden.

Quelle: Academind


0

Vielen Dank für eine wirklich gute Diskussion, aber als echter Neuling frage ich mich, wo Anweisungen aufbewahrt werden? Am Anfang entschieden sich Wissenschaftler zwischen zwei Architekturen (von NEUMANN, wo alles als DATA betrachtet wird, und HARVARD, wo ein Speicherbereich für Anweisungen und ein anderer für Daten reserviert war). Letztendlich haben wir uns für das von Neumann-Design entschieden und jetzt gilt alles als "gleich". Dies machte es mir schwer, als ich Assembly https://www.cs.virginia.edu/~evans/cs216/guides/x86.html lernte, weil sie über Register und Stapelzeiger sprechen.

Alles oben spricht über DATEN. Ich vermute, dass ein Befehl, da er eine definierte Sache mit einem bestimmten Speicherbedarf ist, auf dem Stapel abgelegt wird und sich daher alle in der Assembly diskutierten 'diese' Register auf dem Stapel befinden. Natürlich kam dann die objektorientierte Programmierung mit Anweisungen und Daten, die in eine dynamische Struktur kamen, sodass jetzt auch Anweisungen auf dem Heap bleiben würden?

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.