Könnte es für Systeme im Allgemeinen effizienter sein, Stacks zu beseitigen und Heap nur für die Speicherverwaltung zu verwenden?

14

Es scheint mir, dass alles, was mit einem Stapel gemacht werden kann, mit dem Haufen gemacht werden kann, aber nicht alles, was mit dem Haufen gemacht werden kann, kann mit dem Stapel gemacht werden. Ist das korrekt? Dann, der Einfachheit halber, und selbst wenn wir bei bestimmten Workloads ein wenig an Leistung verlieren, könnte es nicht besser sein, nur einen Standard (dh den Heap) zu verwenden?

Denken Sie an den Kompromiss zwischen Modularität und Leistung. Ich weiß, dass dies nicht der beste Weg ist, um dieses Szenario zu beschreiben, aber im Allgemeinen scheint es, dass die Einfachheit des Verstehens und des Designs eine bessere Option sein könnte, selbst wenn das Potenzial für eine bessere Leistung besteht.

Dunkler Templer
quelle
1
In C und C ++ müssen Sie die Zuordnung des auf dem Heap zugewiesenen Speichers explizit aufheben. Einfacher geht es nicht.
user16764
Ich habe eine C # -Implementierung verwendet, bei der die Profilerstellung ergab, dass Stapelobjekte in einem haufenartigen Bereich mit einer fürchterlichen Speicherbereinigung zugewiesen wurden. Meine Lösung? Verschieben Sie alles Mögliche (z. B. Schleifenvariablen, temporäre Variablen usw.) in den permanenten Heapspeicher. Das Programm hat das 10-fache des Arbeitsspeichers verbraucht und läuft mit der 10-fachen Geschwindigkeit.
Imallett
@ IanMallett: Ich verstehe Ihre Erklärung des Problems und der Lösung nicht. Haben Sie irgendwo einen Link mit mehr Informationen? Normalerweise finde ich die stapelbasierte Zuordnung schneller.
Frank Hileman
@FrankHileman Das Grundproblem war: Die Implementierung von C #, das ich verwendete, hatte eine extrem schlechte Speicherbereinigungsgeschwindigkeit. Die "Lösung" bestand darin, alle Variablen persistent zu machen, so dass zur Laufzeit keine Speicheroperationen stattfanden. Ich habe vor einiger Zeit eine Stellungnahme zur C # / XNA-Entwicklung im Allgemeinen verfasst, in der auch einige Aspekte des Kontexts erörtert werden.
Imallett
@ IanMallett: Danke. Als ehemaliger C / C ++ - Entwickler, der heutzutage hauptsächlich C # verwendet, habe ich ganz andere Erfahrungen gemacht. Ich finde, dass Bibliotheken das größte Problem sind. Es hört sich so an, als ob die XBox360-Plattform für .net-Entwickler nur halbherzig wäre. Normalerweise wechsle ich bei GC-Problemen zum Pooling. Es hilft.
Frank Hileman

Antworten:

30

Haufen sind schlecht bei schneller Speicherzuweisung und Freigabe. Wenn Sie für einen begrenzten Zeitraum viele kleine Mengen an Arbeitsspeicher abrufen möchten, ist ein Heap nicht die beste Wahl. Ein Stack mit seinem supereinfachen Allocation / Deallocation-Algorithmus ist natürlich das Beste (noch mehr, wenn er in die Hardware integriert ist), weshalb die Leute ihn zum Beispiel zum Übergeben von Argumenten an Funktionen und zum Speichern lokaler Variablen verwenden - am häufigsten Ein wichtiger Nachteil ist, dass der Platz begrenzt ist. Daher sind sowohl das Aufbewahren großer Objekte als auch der Versuch, diese für langlebige Objekte zu verwenden, schlechte Ideen.

Den Stack vollständig zu entfernen, um eine Programmiersprache zu vereinfachen, ist der falsche Weg. IMO - ein besserer Ansatz wäre, die Unterschiede zu abstrahieren und den Compiler herausfinden zu lassen, welche Art von Speicher verwendet werden soll, während der Programmierer höhere Programmiersprachen zusammensetzt. Level-Konstrukte, die der Denkweise der Menschen näher kommen - und in der Tat tun High-Level-Sprachen wie C #, Java, Python usw. genau das. Sie bieten eine nahezu identische Syntax für Heap-zugewiesene Objekte und Stack-zugewiesene Primitive ('Referenztypen' vs. 'Werttypen' in .NET Lingo), entweder vollständig transparent oder mit einigen funktionalen Unterschieden, die Sie verstehen müssen, um die Sprache zu verwenden richtig (aber Sie müssen eigentlich nicht wissen, wie ein Stack und ein Heap intern funktionieren).

tdammers
quelle
2
WOW das war gut :) Wirklich kurz und informativ für einen Anfänger!
Dark Templar
1
Auf vielen CPUs wird der Stack in Hardware gehandhabt, was ein Problem außerhalb der Sprache ist, aber zur Laufzeit eine große Rolle spielt.
Patrick Hughes
@Patrick Hughes: Ja, aber der Heap befindet sich auch in der Hardware, nicht wahr?
Dark Templar
@Dark Was Patrick wahrscheinlich sagen möchte, ist, dass Architekturen wie x86 spezielle Register zum Verwalten des Stapels und spezielle Anweisungen zum Einfügen oder Entfernen von etwas auf / aus dem Stapel haben. Das macht es ziemlich schnell.
FUZxxl
3
@Donal Fellows: Alles wahr. Der springende Punkt ist jedoch, dass sowohl Stapel als auch Haufen ihre Stärken und Schwächen haben. Wenn Sie sie dementsprechend verwenden, erhalten Sie den effizientesten Code.
tdammers
8

Einfach ausgedrückt, ein Stack ist kein bisschen Leistung. Es ist hunderte oder tausende Male schneller als der Haufen. Darüber hinaus bieten die meisten modernen Computer Hardware-Unterstützung für den Stack (wie x86), und die Hardwarefunktionalität für z. B. den Aufruf-Stack kann nicht entfernt werden.

DeadMG
quelle
Was meinen Sie, wenn Sie sagen, dass moderne Maschinen Hardware-Unterstützung für den Stapel haben? Der Stack selbst ist schon in der Hardware, oder?
Dunkle Templer
1
x86 verfügt über spezielle Register und Anweisungen für den Umgang mit dem Stapel. x86 unterstützt keine Heaps - solche Dinge werden vom Betriebssystem erstellt.
Pubby
8

Nein

Der Stapelbereich in C ++ ist im Vergleich unglaublich schnell. Ich gehe davon aus, dass erfahrene C ++ - Entwickler diese Funktionalität nicht deaktivieren können.

Mit C ++ haben Sie die Wahl und Sie haben die Kontrolle. Die Designer waren nicht besonders geneigt, Funktionen einzuführen, die die Ausführungszeit oder den Ausführungsraum erheblich verlängerten.

Diese Wahl treffen

Wenn Sie eine Bibliothek oder ein Programm erstellen möchten, für das jedes Objekt dynamisch zugewiesen werden muss, können Sie dies mit C ++ tun. Es würde relativ langsam ablaufen, aber Sie könnten dann diese "Modularität" haben. Für den Rest von uns ist die Modularität immer optional. Führen Sie sie nach Bedarf ein, da beide für eine gute / schnelle Implementierung erforderlich sind.

Alternativen

Es gibt andere Sprachen, die erfordern, dass der Speicher für jedes Objekt auf dem Heap erstellt wird. es ist ziemlich langsam, so dass es Designs (reale Programme) auf eine Weise kompromittiert, die schlimmer ist als beides lernen zu müssen (IMO).

Beides ist wichtig, und mit C ++ können Sie beide Funktionen für jedes Szenario effektiv nutzen. Trotzdem ist die C ++ - Sprache möglicherweise nicht ideal für Ihr Design, wenn diese Faktoren in Ihrem OP für Sie wichtig sind (lesen Sie beispielsweise in höheren Sprachen nach).

Justin
quelle
Der Heap hat tatsächlich die gleiche Geschwindigkeit wie der Stack, verfügt jedoch nicht über die spezielle Hardware-Unterstützung für die Zuweisung. Auf der anderen Seite gibt es Möglichkeiten, Haufen stark zu beschleunigen (unter einer Reihe von Bedingungen, die sie zu Nur-Experten-Techniken machen).
Donal Fellows
@DonalFellows: Die Hardware-Unterstützung für Stacks ist irrelevant. Was wichtig ist, ist zu wissen, dass jedes Mal, wenn etwas freigegeben wird, etwas freigegeben werden kann, das danach zugewiesen wurde. Einige Programmiersprachen verfügen nicht über Heaps, mit denen Objekte unabhängig voneinander freigegeben werden können, sondern nur über die Methode "Alles freigeben nach".
Supercat
6

Dann, der Einfachheit halber, und selbst wenn wir bei bestimmten Workloads ein wenig an Leistung verlieren, könnte es nicht besser sein, nur einen Standard (dh den Heap) zu verwenden?

Eigentlich dürfte der Leistungseinbruch beachtlich sein!

Wie andere betont haben, handelt es sich bei Stacks um eine äußerst effiziente Struktur für die Verwaltung von Daten, die den LIFO-Regeln (Last-In-First-Out) entsprechen. Die Speicherzuweisung / -freigabe auf dem Stapel ist normalerweise nur eine Änderung an einem Register in der CPU. Das Ändern eines Registers ist fast immer eine der schnellsten Operationen, die ein Prozessor ausführen kann.

Der Heap ist normalerweise eine ziemlich komplexe Datenstruktur, und das Zuweisen / Freigeben von Speicher erfordert viele Anweisungen, um die gesamte zugehörige Buchhaltung durchzuführen. Schlimmer noch, in allgemeinen Implementierungen kann jeder Aufruf zur Arbeit mit dem Heap zu einem Aufruf des Betriebssystems führen. Betriebssystemaufrufe sind sehr zeitaufwändig! Das Programm muss normalerweise vom Benutzermodus in den Kernelmodus wechseln. In diesem Fall kann das Betriebssystem entscheiden, dass andere Programme dringendere Anforderungen haben und dass Ihr Programm warten muss.

Charles E. Grant
quelle
5

Simula benutzte den Haufen für alles. Alles auf dem Heap Putting induziert immer noch ein Dereferenzierungsebene für lokale Variablen, und es bringt zusätzlichen Druck auf den Garbage Collector (man muss berücksichtigen , dass Garbage Collectors wirklich damals gesaugt). Das ist auch der Grund, warum Bjarne C ++ erfunden hat.

fredoverflow
quelle
Also verwendet C ++ grundsätzlich nur den Heap?
Dark Templar
2
@ Dark: Was? Nein. Der Mangel an Stack in Simula war eine Inspiration, um es besser zu machen.
Fredoverflow
Ah, ich verstehe, was du jetzt meinst! Thanks +1 :)
Dark Templar
3

Stapel sind äußerst effizient für LIFO-Daten, wie z. B. die mit Funktionsaufrufen verbundenen Metadaten. Der Stack nutzt auch die inhärenten Designmerkmale der CPU. Da die Leistung auf dieser Ebene für fast alles andere in einem Prozess von grundlegender Bedeutung ist, wird sich die Annahme dieses "kleinen" Treffers auf dieser Ebene sehr weit verbreiten. Darüber hinaus kann der Heapspeicher vom Betriebssystem verschoben werden, was für Stacks tödlich wäre. Während ein Stapel im Heap implementiert werden kann, ist ein Overhead erforderlich, der buchstäblich jeden Teil eines Prozesses auf der detailliertesten Ebene betrifft.

kylben
quelle
2

"effizient" in Bezug auf das Schreiben von Code, aber sicherlich nicht in Bezug auf die Effizienz Ihrer Software. Die Stapelzuweisungen sind im Wesentlichen frei (es sind nur wenige Maschinenanweisungen erforderlich, um den Stapelzeiger zu verschieben und Platz auf dem Stapel für lokale Variablen zu reservieren).

Da die Stapelzuweisung fast keine Zeit in Anspruch nimmt, ist eine Zuweisung selbst auf einem sehr effizienten Heap 100 KB (wenn nicht mehr als 1 MB) Mal langsamer.

Stellen Sie sich nun vor, wie viele lokale Variablen und andere Datenstrukturen eine typische Anwendung verwendet. Jedes einzelne kleine "i", das Sie als Schleifenzähler verwenden, wird millionenfach langsamer zugeordnet.

Sicher, wenn die Hardware schnell genug ist, können Sie eine Anwendung schreiben, die nur Heap verwendet. Aber stellen Sie sich jetzt vor, welche Art von Anwendung Sie schreiben könnten, wenn Sie Heap nutzen und dieselbe Hardware verwenden würden.

DXM
quelle
Wenn Sie sagen "Stellen Sie sich vor, wie viele lokale Variablen und andere Datenstrukturen eine typische Anwendung verwendet", auf welche anderen Datenstrukturen beziehen Sie sich konkret?
Dunkler Templer
1
Sind die Werte "100k" und "1M +" irgendwie wissenschaftlich? Oder ist es nur eine Möglichkeit, "viel" zu sagen?
Bruno Reis
@Bruno - meiner Meinung nach sind die von mir verwendeten 100K- und 1M-Zahlen eine konservative Schätzung, um einen Punkt zu belegen. Wenn Sie mit VS und C ++ vertraut sind, schreiben Sie ein Programm, das 100 Bytes auf dem Stapel zuweist, und schreiben Sie ein Programm, das 100 Bytes auf dem Heap zuweist. Wechseln Sie dann in die Demontageansicht und zählen Sie einfach die Anzahl der Montageanweisungen für jede Zuordnung. Heap-Vorgänge bestehen in der Regel aus mehreren Funktionsaufrufen in die Windows-DLL, Buckets und verknüpften Listen sowie Zusammenführungs- und anderen Algorithmen. Mit Stack kann es sich auf eine Montageanweisung "add esp, 100" beschränken ...
DXM
2
"100k (wenn nicht 1M +) mal langsamer"? Das ist ziemlich übertrieben. Lassen Sie es zwei Größenordnungen langsamer sein, vielleicht drei, aber das war's. Zumindest kann mein Linux 100 Millionen Heap-Zuweisungen (+ einige umgebende Anweisungen) in weniger als 6 Sekunden auf einem Core i5 ausführen, das können nicht mehr als ein paar hundert Anweisungen pro Zuweisung sein - tatsächlich ist es mit ziemlicher Sicherheit weniger. Wenn es sechs Größenordnungen langsamer als Stack ist, stimmt etwas nicht mit der Heap-Implementierung des Betriebssystems. Sicher, es gibt eine Menge
Probleme
1
Moderatoren sind wahrscheinlich dabei, diesen ganzen Kommentarthread abzubrechen. Also hier ist der Deal, ich gebe zu, dass die tatsächlichen Zahlen aus meinem ... herausgezogen wurden, aber lasst uns zustimmen, dass der Faktor wirklich sehr, sehr groß ist und keine weiteren Kommentare macht :)
DXM
2

Möglicherweise interessieren Sie sich für "Garbage Collection ist schnell, aber ein Stapel ist schneller".

http://dspace.mit.edu/bitstream/handle/1721.1/6622/AIM-1462.ps.Z

Wenn ich es richtig lese, haben diese Leute einen C-Compiler modifiziert, um "Stack-Frames" auf dem Heap zuzuweisen, und dann die Garbage Collection verwendet, um die Zuordnung der Frames aufzuheben, anstatt den Stack zu löschen.

Stack-zugewiesene "Stack-Frames" übertreffen die Heap-zugewiesenen "Stack-Frames" entscheidend.

Bruce Ediger
quelle
1

Wie wird der Aufrufstapel auf einem Haufen funktionieren? Im Wesentlichen müssten Sie in jedem Programm einen Stapel auf dem Heap zuweisen. Warum sollte die OS + -Hardware das nicht für Sie tun?

Wenn Sie möchten, dass die Dinge wirklich einfach und effizient sind, geben Sie dem Benutzer einfach einen Teil des Arbeitsspeichers und lassen Sie ihn damit umgehen. Natürlich will niemand alles selbst implementieren und deshalb haben wir einen Stack und einen Haufen.

Pubby
quelle
Genau genommen ist ein "Aufrufstapel" in einer Laufzeitumgebung für Programmiersprachen nicht erforderlich. Beispiel: Eine einfache Implementierung einer trägen funktionalen Sprache durch Graph Reduction (die ich codiert habe) hat keinen Aufrufstapel. Der Aufrufstapel ist jedoch eine sehr nützliche und weit verbreitete Technik, zumal moderne Prozessoren davon ausgehen, dass Sie ihn verwenden, und für seine Verwendung optimiert sind.
Ben
@Ben - während es wahr ist (und eine gute Sache ist), Dinge wie die Speicherzuweisung aus einer Sprache heraus zu abstrahieren, ändert dies nichts an der jetzt vorherrschenden Computerarchitektur. Daher verwendet Ihr Grafikverkleinerungscode den Stapel weiterhin, wenn er ausgeführt wird - ob es Ihnen gefällt oder nicht.
Ingo
@Ingo Nicht wirklich im eigentlichen Sinne. Sicher, das Betriebssystem initialisiert einen Abschnitt des Speichers, der traditionell als "Stapel" bezeichnet wird, und es wird ein Register geben, das darauf verweist. Funktionen in der Ausgangssprache werden jedoch nicht als Stapelrahmen in der Aufrufreihenfolge dargestellt. Die Funktionsausführung wird vollständig durch die Manipulation von Datenstrukturen im Heap dargestellt. Auch ohne die Last-Call-Optimierung ist ein "Überlauf des Stacks" nicht möglich. Das ist es, was ich meine, wenn ich sage, dass "der Aufrufstapel" nichts Grundlegendes ist.
Ben
Ich spreche nicht von den Funktionen der Ausgangssprache, sondern von den Funktionen im Interpreter (oder was auch immer), die die Grafikverkleinerung tatsächlich durchführen. Die brauchen einen Stapel. Dies ist offensichtlich, da zeitgenössische Hardware die Grafik nicht reduziert. Daher wird Ihr Graph-Reduktions-Algorithmus letztendlich auf den Maschinen-Ode abgebildet, und ich wette, es gibt Unterprogrammaufrufe unter ihnen. QED.
Ingo
1

Sowohl Stack als auch Heap sind erforderlich. Sie werden in verschiedenen Situationen verwendet, zum Beispiel:

  1. Die Heap-Zuweisung hat eine Einschränkung: sizeof (a [0]) == sizeof (a [1])
  2. Die Stapelzuweisung unterliegt der Einschränkung, dass sizeof (a) die Kompilierungszeitkonstante ist
  3. Die Heap-Zuweisung kann Schleifen, Grafiken usw. komplexer Datenstrukturen ausführen
  4. Die Stapelzuweisung kann Bäume in Kompilierzeitgröße ausführen
  5. Heap erfordert die Verfolgung des Eigentums
  6. Die Stapelzuweisung und Freigabe erfolgt automatisch
  7. Der Heapspeicher kann einfach über Zeiger von einem Bereich zu einem anderen übergeben werden
  8. Der Stapelspeicher ist für jede Funktion lokal, und Objekte müssen in den oberen Bereich verschoben werden, um ihre Lebensdauer zu verlängern (oder in Objekten statt in Elementfunktionen gespeichert werden).
  9. Haufen ist schlecht für die Leistung
  10. Stack ist ziemlich schnell
  11. Heap-Objekte werden von Funktionen über Zeiger zurückgegeben, die den Besitz übernehmen. Oder shared_ptrs.
  12. Stapelobjekte werden von Funktionen über Verweise zurückgegeben, die keinen Besitz übernehmen.
  13. Heap erfordert, dass jeder neue mit der richtigen Art des Löschens oder Löschens abgeglichen wird []
  14. Stapelobjekte verwenden RAII- und Konstruktorinitialisierungslisten
  15. Heap-Objekte können an jedem Punkt innerhalb einer Funktion initialisiert werden und keine Konstruktorparameter verwenden
  16. Stapelobjekte verwenden Konstruktorparameter zur Initialisierung
  17. Heap verwendet Arrays und die Arraygröße kann sich zur Laufzeit ändern
  18. Der Stapel gilt für einzelne Objekte, und die Größe ist auf die Kompilierungszeit festgelegt

Grundsätzlich sind die Mechanismen gar nicht vergleichbar, weil so viele Details unterschiedlich sind. Das einzige, was sie gemeinsam haben, ist, dass sie beide irgendwie mit dem Gedächtnis umgehen.

tp1
quelle
1

Moderne Computer verfügen neben einem großen, aber langsamen Hauptspeichersystem über mehrere Cache-Speicherschichten. In der Zeit, die zum Lesen oder Schreiben eines Bytes aus dem Hauptspeichersystem erforderlich ist, können Dutzende Zugriffe auf den schnellsten Cache-Speicher ausgeführt werden. Der tausendfache Zugriff auf einen Standort ist somit viel schneller als der einmalige Zugriff auf 1.000 (oder sogar 100) unabhängige Standorte. Da die meisten Anwendungen wiederholt kleine Speichermengen in der Nähe des oberen Bereichs des Stapels zuweisen und freigeben, werden die Positionen auf dem oberen Bereich des Stapels in enormen Mengen verwendet und wiederverwendet, so dass die überwiegende Mehrheit (99% + in einer typischen Anwendung) Stapelzugriffe können über den Cache-Speicher abgewickelt werden.

Im Gegensatz dazu müsste, wenn eine Anwendung wiederholt Heap-Objekte zum Speichern von Fortsetzungsinformationen erstellen und aufgeben würde, jede Version jedes jemals erstellten Stack-Objekts in den Hauptspeicher geschrieben werden. Selbst wenn die überwiegende Mehrheit solcher Objekte zu dem Zeitpunkt, an dem die CPU die Cache-Seiten, in denen sie begonnen hatten, recyceln wollte, völlig unbrauchbar wäre, hätte die CPU keine Möglichkeit, dies zu wissen. Infolgedessen müsste die CPU viel Zeit damit verschwenden, langsame Speicherschreibvorgänge mit nutzlosen Informationen durchzuführen. Nicht gerade ein Rezept für Geschwindigkeit.

In vielen Fällen ist es hilfreich zu wissen, dass eine an eine Routine übergebene Objektreferenz nach dem Beenden der Routine nicht mehr verwendet wird. Wenn Parameter und lokale Variablen über den Stapel übergeben werden und eine Überprüfung des Codes der Routine ergibt, dass keine Kopie der übergebenen Referenz erhalten bleibt, kann der Code, der die Routine aufruft, sicher sein, dass keine externe Referenz auf die Routine vorhanden ist Objekt existierte vor dem Aufruf, danach existiert kein Objekt mehr. Wenn hingegen Parameter über Heap-Objekte übergeben würden, würden Konzepte wie "Nach Rückkehr einer Routine" etwas nebulöser, da die Routine, wenn der Code eine Kopie der Fortsetzung aufbewahrt, nach a mehr als einmal "zurückkehren" könnte einzelner Anruf.

Superkatze
quelle