it-swarm.com.de

Was ist schneller: Stack Allocation oder Heap Allocation

Diese Frage mag ziemlich elementar klingen, aber dies ist eine Debatte, die ich mit einem anderen Entwickler geführt habe, mit dem ich arbeite.

Ich achtete darauf, Dinge so zu stapeln, wie ich konnte, anstatt sie zu haufen. Er sprach mit mir und beobachtete mich über die Schulter und bemerkte, dass es nicht notwendig sei, weil sie in Bezug auf die Leistung gleich sind.

Ich hatte immer den Eindruck, dass das Wachsen des Stapels eine konstante Zeit ist und die Leistung der Heap-Zuordnung von der aktuellen Komplexität des Heaps abhängt, sowohl für die Zuordnung (Finden eines Lochs der richtigen Größe) als auch für das Aufheben der Zuordnung (Reduzieren von Löchern, um die Fragmentierung zu verringern) Viele Standard-Bibliotheksimplementierungen benötigen Zeit, um dies während des Löschens zu tun, wenn ich mich nicht irre.

Dies erscheint mir als etwas, das wahrscheinlich sehr vom Compiler abhängig wäre. Insbesondere für dieses Projekt verwende ich einen Metrowerks - Compiler für die PPC - Architektur. Einsicht in diese Kombination wäre am hilfreichsten, aber was ist im Allgemeinen für GCC und MSVC++ der Fall? Ist die Heap-Zuweisung nicht so leistungsfähig wie die Stack-Zuweisung? Gibt es keinen Unterschied? Oder sind die Unterschiede so gering, dass es zu einer sinnlosen Mikrooptimierung kommt?.

484
Adam

Die Stapelzuweisung ist viel schneller, da nur der Stapelzeiger bewegt wird. Mithilfe von Speicherpools können Sie eine vergleichbare Leistung bei der Heap-Zuweisung erzielen, was jedoch mit einer geringfügig erhöhten Komplexität und eigenen Kopfschmerzen verbunden ist.

Stack vs. Heap ist nicht nur ein Leistungsaspekt. es sagt auch viel über die erwartete Lebensdauer von Objekten aus.

478

Stack ist viel schneller. Es wird buchstäblich nur ein einziger Befehl auf den meisten Architekturen verwendet, in den meisten Fällen, z. auf x86:

sub esp, 0x10

(Das verschiebt den Stapelzeiger um 0x10 Bytes nach unten und "reserviert" diese Bytes für die Verwendung durch eine Variable.)

Natürlich ist die Größe des Stapels sehr, sehr begrenzt, da Sie schnell herausfinden werden, ob Sie die Stapelzuordnung überbeanspruchen oder versuchen, eine Rekursion durchzuführen :-)

Es gibt auch wenig Grund, die Leistung von Code zu optimieren, für den dies nicht nachweislich erforderlich ist, wie z. B. durch die Profilerstellung. "Vorzeitige Optimierung" verursacht oft mehr Probleme als es wert ist.

Meine Faustregel: Wenn ich weiß, dass ich Daten benötigen werde zur Kompilierungszeit und diese weniger als ein paar hundert Bytes groß sind, teile ich sie stapelweise zu. Ansonsten teile ich es haufenweise zu.

163
Dan Lenski

Ehrlich gesagt ist es trivial, ein Programm zu schreiben, um die Leistung zu vergleichen:

#include <ctime>
#include <iostream>

namespace {
    class empty { }; // even empty classes take up 1 byte of space, minimum
}

int main()
{
    std::clock_t start = std::clock();
    for (int i = 0; i < 100000; ++i)
        empty e;
    std::clock_t duration = std::clock() - start;
    std::cout << "stack allocation took " << duration << " clock ticks\n";
    start = std::clock();
    for (int i = 0; i < 100000; ++i) {
        empty* e = new empty;
        delete e;
    };
    duration = std::clock() - start;
    std::cout << "heap allocation took " << duration << " clock ticks\n";
}

Es wird gesagt, dass eine dumme Konsequenz ist der Hobgoblin der kleinen Geister . Offensichtlich optimierende Compiler sind die Hobgoblins vieler Programmierer. Früher stand diese Diskussion am Ende der Antwort, aber die Leute können sich anscheinend nicht die Mühe machen, so weit zu lesen. Deshalb gehe ich hier nach oben, um zu vermeiden, dass ich bereits beantwortete Fragen bekomme.

Ein optimierender Compiler bemerkt möglicherweise, dass dieser Code nichts tut, und optimiert möglicherweise alles weg. Es ist die Aufgabe des Optimierers, solche Dinge zu tun, und gegen den Optimierer zu kämpfen, ist eine dumme Angelegenheit.

Ich würde empfehlen, diesen Code mit deaktivierter Optimierung zu kompilieren, da es keine gute Möglichkeit gibt, jeden derzeit verwendeten oder zukünftig verwendeten Optimierer zu täuschen.

Wer den Optimierer einschaltet und sich dann darüber beschwert, dass er bekämpft wird, sollte öffentlich lächerlich gemacht werden.

Wenn ich mich um die Genauigkeit von Nanosekunden kümmern würde, würde ich std::clock() nicht verwenden. Wenn ich die Ergebnisse als Doktorarbeit veröffentlichen wollte, würde ich diesbezüglich einen größeren Unterschied machen und wahrscheinlich GCC, Tendra/Ten15, LLVM, Watcom, Borland, Visual C++, Digital Mars, ICC und andere Compiler vergleichen. Derzeit dauert die Heap-Zuweisung hunderte Male länger als die Stack-Zuweisung, und ich sehe keinen Sinn darin, die Frage weiter zu untersuchen.

Das Optimierungsprogramm hat die Aufgabe, den Code, den ich teste, zu entfernen. Ich sehe keinen Grund, dem Optimierer anzuweisen, ausgeführt zu werden, und dann zu versuchen, den Optimierer zum Narren zu halten, damit er nicht wirklich optimiert. Wenn ich dies jedoch als sinnvoll erachte, würde ich eine oder mehrere der folgenden Maßnahmen ergreifen:

  1. Fügen Sie ein Datenelement zu empty hinzu und greifen Sie auf dieses Datenelement in der Schleife zu. aber wenn ich nur jemals aus dem Datenelement gelesen habe, kann der Optimierer konstant falten und die Schleife entfernen; Wenn ich nur jemals in das Datenelement schreibe, überspringt der Optimierer möglicherweise alle bis auf die allerletzte Iteration der Schleife. Außerdem lautete die Frage nicht "Stapelzuweisung und Datenzugriff im Vergleich zu Heapzuweisung und Datenzugriff".

  2. Deklarieren Sie evolatile, aber volatile wird häufig falsch kompiliert (PDF).

  3. Nehmen Sie die Adresse von e in die Schleife (und weisen Sie sie möglicherweise einer Variablen zu, die als extern deklariert und in einer anderen Datei definiert ist). Aber selbst in diesem Fall kann der Compiler feststellen, dass - zumindest auf dem Stack - e immer an der gleichen Speicheradresse zugewiesen wird und dann eine konstante Faltung wie in (1) oben ausgeführt wird. Ich erhalte alle Iterationen der Schleife, aber das Objekt wird nie tatsächlich zugewiesen.

Über das Offensichtliche hinaus ist dieser Test insofern fehlerhaft, als er sowohl die Zuordnung als auch die Freigabe misst, und die ursprüngliche Frage stellte keine Frage nach der Freigabe. Natürlich werden auf dem Stack zugewiesene Variablen am Ende ihres Gültigkeitsbereichs automatisch freigegeben. Wenn Sie also nicht delete aufrufen, werden die Zahlen (1) verzerrt Measure Heap Deallocation) und (2) verursachen einen ziemlich schlechten Speicherverlust, es sei denn, wir behalten einen Verweis auf den neuen Zeiger bei und rufen delete auf, nachdem wir unsere Zeitmessung durchgeführt haben.

Auf meinem Computer mit g ++ 3.4.4 unter Windows erhalte ich "0 Clock Ticks" für die Stapel- und Heapzuweisung für weniger als 100000 Zuweisungen, und selbst dann erhalte ich "0 Clock Ticks" für die Stapelzuweisung und "15 Clock Ticks" "für die Heap - Zuordnung. Wenn ich 10.000.000 Zuweisungen messe, dauert die Stapelzuweisung 31 Takt-Ticks und die Heap-Zuweisung 1562 Takt-Ticks.


Ja, ein optimierender Compiler kann sich die Erstellung der leeren Objekte ersparen. Wenn ich es richtig verstehe, kann es sogar die gesamte erste Schleife übergehen. Als ich die Iterationen auf 10.000.000 erhöht habe, dauerte die Stapelzuweisung 31 Uhr-Ticks und die Heap-Zuweisung 1562 Uhr-Ticks. Ich denke, es ist sicher zu sagen, dass, ohne g ++ zu sagen, um die ausführbare Datei zu optimieren, g ++ die Konstruktoren nicht entmutigt hat.


In den Jahren, seit ich das geschrieben habe, war es die Vorliebe für Stack Overflow, die Leistung von optimierten Builds zu veröffentlichen. Im Allgemeinen halte ich das für richtig. Ich halte es jedoch immer noch für dumm, den Compiler zu bitten, den Code zu optimieren, wenn Sie tatsächlich nicht möchten, dass dieser Code optimiert wird. Es kommt mir sehr ähnlich vor, als würde ich für den Parkservice extra bezahlen, aber ich weigere mich, die Schlüssel abzugeben. In diesem speziellen Fall möchte ich nicht, dass der Optimierer ausgeführt wird.

Verwenden einer leicht modifizierten Version des Benchmarks (um den gültigen Punkt zu ermitteln, den das ursprüngliche Programm nicht jedes Mal durch die Schleife auf dem Stack zugewiesen hat) und Kompilieren ohne Optimierung, aber Verknüpfen mit Veröffentlichungsbibliotheken (um den gültigen Punkt zu ermitteln, den wir nicht verwenden) keine Verlangsamung durch Verknüpfung mit Debug-Bibliotheken einbeziehen wollen):

#include <cstdio>
#include <chrono>

namespace {
    void on_stack()
    {
        int i;
    }

    void on_heap()
    {
        int* i = new int;
        delete i;
    }
}

int main()
{
    auto begin = std::chrono::system_clock::now();
    for (int i = 0; i < 1000000000; ++i)
        on_stack();
    auto end = std::chrono::system_clock::now();

    std::printf("on_stack took %f seconds\n", std::chrono::duration<double>(end - begin).count());

    begin = std::chrono::system_clock::now();
    for (int i = 0; i < 1000000000; ++i)
        on_heap();
    end = std::chrono::system_clock::now();

    std::printf("on_heap took %f seconds\n", std::chrono::duration<double>(end - begin).count());
    return 0;
}

zeigt an:

on_stack took 2.070003 seconds
on_heap took 57.980081 seconds

auf meinem System, wenn mit der Befehlszeile kompiliert cl foo.cc /Od /MT /EHsc.

Sie stimmen möglicherweise nicht mit meinem Ansatz überein, einen nicht optimierten Build zu erhalten. Das ist in Ordnung: Sie können den Benchmark beliebig ändern. Wenn ich die Optimierung einschalte, erhalte ich:

on_stack took 0.000000 seconds
on_heap took 51.608723 seconds

Nicht, weil die Stapelzuweisung tatsächlich sofort erfolgt, sondern weil jeder halbwegs vernünftige Compiler feststellen kann, dass on_stack Nichts Sinnvolles tut und optimiert werden kann. GCC auf meinem Linux-Laptop bemerkt auch, dass on_heap Nichts Nützliches tut und optimiert es auch weg:

on_stack took 0.000003 seconds
on_heap took 0.000002 seconds
115
Max Lybbert

Eine interessante Sache, die ich über Stack vs. Heap Allocation auf dem Xbox 360 Xenon-Prozessor erfahren habe, die auch auf andere Multicore-Systeme angewendet werden kann, ist, dass beim Zuweisen auf dem Heap ein kritischer Abschnitt eingegeben wird, um alle anderen Kerne anzuhalten, damit die Zuweisung nicht erfolgt nicht widersprechen. Daher war Stack Allocation in einer engen Schleife der richtige Weg für Arrays mit fester Größe, da Stalls verhindert wurden.

Dies kann eine weitere zu berücksichtigende Geschwindigkeit sein, wenn Sie für Multicore/Multiproc codieren, da Ihre Stapelzuordnung nur für den Kern sichtbar ist, auf dem Ihre Bereichsfunktion ausgeführt wird, und andere Kerne/CPUs davon nicht betroffen sind.

29
Furious Coder

Sie können einen speziellen Heap-Allokator für bestimmte Größen von Objekten schreiben, der sehr leistungsfähig ist. Der Heap-Allokator allgemein ist jedoch nicht besonders performant.

Ich stimme auch Torbjörn Gyllebring über die erwartete Lebensdauer von Objekten zu. Guter Punkt!

18

Ich denke nicht, dass Stapelzuweisung und Heapzuweisung im Allgemeinen austauschbar sind. Ich hoffe auch, dass die Leistung von beiden für den allgemeinen Gebrauch ausreicht.

Ich würde es wärmstens für kleine Gegenstände empfehlen, je nachdem, welches für den Umfang der Zuordnung besser geeignet ist. Für große Gegenstände ist der Haufen wahrscheinlich notwendig.

Auf 32-Bit-Betriebssystemen mit mehreren Threads ist der Stapel häufig eher begrenzt (wenn auch in der Regel auf mindestens einige MB), da der Adressraum aufgeteilt werden muss und früher oder später ein Thread-Stapel in einen anderen ausgeführt wird. Auf Single-Thread-Systemen (Linux-glibc-Single-Thread) ist die Einschränkung viel geringer, da der Stack einfach wachsen und wachsen kann.

Auf 64-Bit-Betriebssystemen ist genügend Adressraum vorhanden, um Thread-Stapel recht groß zu machen.

7
MarkR

Normalerweise besteht die Stapelzuweisung nur aus dem Subtrahieren vom Stapelzeigerregister. Das ist viel schneller als das Durchsuchen eines Haufens.

Manchmal erfordert die Stapelzuweisung das Hinzufügen einer Seite (n) virtuellen Speichers. Das Hinzufügen einer neuen Seite mit null Speicher erfordert kein Lesen einer Seite von der Festplatte, daher ist dies normalerweise immer noch viel schneller als das Durchsuchen eines Heaps (insbesondere, wenn ein Teil des Heaps auch ausgelagert wurde). In einer seltenen Situation, und Sie könnten ein solches Beispiel konstruieren, ist gerade in einem Teil des Heaps, der sich bereits im RAM befindet, genügend Speicherplatz verfügbar, aber das Zuweisen einer neuen Seite für den Stapel muss warten, bis eine andere Seite ausgeschrieben wird auf die Festplatte. In dieser seltenen Situation ist der Haufen schneller.

6

Abgesehen von dem Leistungsvorteil in der Größenordnung gegenüber der Heap-Zuweisung ist die Stapelzuweisung für Serveranwendungen mit langer Laufzeit vorzuziehen. Selbst die am besten verwalteten Heaps werden schließlich so fragmentiert, dass sich die Anwendungsleistung verschlechtert.

6
Jay

Ein Stack hat eine begrenzte Kapazität, ein Heap hingegen nicht. Der typische Stapel für einen Prozess oder Thread liegt bei ca. 8 KB. Sie können die zugewiesene Größe nicht mehr ändern.

Eine Stack-Variable folgt den Scoping-Regeln, eine Heap-Variable hingegen nicht. Wenn Ihr Anweisungszeiger über eine Funktion hinausgeht, verschwinden alle neuen Variablen, die der Funktion zugeordnet sind.

Am wichtigsten ist jedoch, dass Sie die gesamte Funktionsaufrufkette nicht im Voraus vorhersagen können. Eine Zuweisung von nur 200 Bytes kann also einen Stapelüberlauf auslösen. Dies ist besonders wichtig, wenn Sie eine Bibliothek schreiben, keine Anwendung.

4
yogman

Es ist nicht nur die Stapelzuweisung, die schneller ist. Sie gewinnen auch viel bei der Verwendung von Stapelvariablen. Sie haben eine bessere Bezugslokalität. Und schließlich ist die Freigabe auch viel billiger.

3
MSalters

Die Stapelzuweisung ist fast immer so schnell oder schneller als die Heapzuweisung, obwohl es für einen Heapzuweiser sicherlich möglich ist, einfach eine stapelbasierte Zuweisungstechnik zu verwenden.

Es gibt jedoch größere Probleme, wenn es um die Gesamtleistung der Stack- oder Heap-basierten Zuweisung geht (oder, etwas besser ausgedrückt, der lokalen oder externen Zuweisung). Normalerweise ist die (externe) Heap-Zuweisung langsam, da viele verschiedene Arten von Zuweisungen und Zuweisungsmustern verarbeitet werden. Wenn Sie den Bereich des verwendeten Allokators reduzieren (lokal für den Algorithmus/Code festlegen), wird die Leistung ohne größere Änderungen tendenziell gesteigert. Durch Hinzufügen einer besseren Struktur zu Ihren Zuordnungsmustern, z. B. durch Erzwingen einer LIFO) - Reihenfolge für Zuordnungs- und Freigabepaare, können Sie auch die Leistung Ihres Zuordners verbessern, indem Sie den Zuordner auf einfachere und strukturiertere Weise verwenden Sie können einen Allokator verwenden oder schreiben, der auf Ihr bestimmtes Allokationsmuster abgestimmt ist: Die meisten Programme weisen häufig einige diskrete Größen zu, sodass ein Heap, der auf einem Lookaside-Puffer mit einigen festen (vorzugsweise bekannten) Größen basiert, eine sehr gute Leistung erbringt -Fragmentierungshaufen aus genau diesem Grund.

Andererseits ist die stapelbasierte Zuweisung in einem 32-Bit-Speicherbereich auch gefährdet, wenn Sie zu viele Threads haben. Stacks benötigen einen zusammenhängenden Speicherbereich. Je mehr Threads Sie haben, desto mehr virtuellen Adressraum benötigen Sie, damit sie ohne Stapelüberlauf ausgeführt werden können. Dies ist (vorerst) kein Problem mit 64-Bit, kann aber in Programmen mit langer Laufzeit und vielen Threads mit Sicherheit Verwüstungen anrichten. Es ist immer mühsam, mit dem Problem umzugehen, dass der virtuelle Adressraum aufgrund von Fragmentierung knapp wird.

3
MSN

Das wahrscheinlich größte Problem bei der Heap-Zuweisung im Vergleich zur Stapelzuweisung ist, dass die Heap-Zuweisung im Allgemeinen eine unbegrenzte Operation ist und Sie sie daher nicht verwenden können, wenn es um das Timing geht.

Bei anderen Anwendungen, bei denen das Timing keine Rolle spielt, spielt es möglicherweise keine Rolle. Wenn Sie jedoch viel Heap zuweisen, wirkt sich dies auf die Ausführungsgeschwindigkeit aus. Versuchen Sie immer, den Stapel für kurzlebigen und häufig zugewiesenen Speicher (z. B. in Schleifen) und so lange wie möglich zu verwenden - führen Sie die Heap-Zuweisung während des Anwendungsstarts durch.

3
larsivi

Ich denke, die Lebensdauer ist entscheidend, und ob das zugeteilte Ding auf komplexe Weise konstruiert werden muss. Beispielsweise müssen Sie in der transaktionsgesteuerten Modellierung normalerweise eine Transaktionsstruktur mit einer Reihe von Feldern ausfüllen und an Operationsfunktionen übergeben. Schauen Sie sich den OSCI SystemC TLM-2.0-Standard als Beispiel an.

Das Zuweisen dieser Daten auf dem Stapel in der Nähe des Aufrufs der Operation verursacht in der Regel einen enormen Overhead, da die Konstruktion teuer ist. Die gute Möglichkeit besteht darin, die Transaktionsobjekte auf dem Heap zuzuweisen und wiederzuverwenden, entweder durch Pooling oder durch eine einfache Richtlinie wie "Dieses Modul benötigt nur ein Transaktionsobjekt je".

Dies ist um ein Vielfaches schneller als die Zuweisung des Objekts bei jedem Operationsaufruf.

Der Grund ist einfach, dass das Objekt eine teure Konstruktion und eine ziemlich lange Nutzungsdauer hat.

Ich würde sagen: Probieren Sie beides aus und sehen Sie, was in Ihrem Fall am besten funktioniert, da es wirklich vom Verhalten Ihres Codes abhängen kann.

3
jakobengblom2

Probleme in Bezug auf die C++ - Sprache

Erstens gibt es keine sogenannte "Stapel" - oder "Haufen" -Zuweisung, die von C++ vorgeschrieben wird. Wenn es sich um automatische Objekte in Blockbereichen handelt, werden sie nicht einmal "zugeordnet". (Übrigens ist die automatische Speicherdauer in C definitiv NICHT gleich "zugewiesen"; letztere ist in der C++ - Sprache "dynamisch".) Der dynamisch zugewiesene Speicher befindet sich im free store, nicht unbedingt auf "the heap", obwohl letzteres oft die (default) implementierung ist.

Obwohl nach den abstract machine semantischen Regeln automatische Objekte immer noch Speicher belegen, darf eine konforme C++ - Implementierung diese Tatsache ignorieren, wenn sie beweisen kann, dass dies keine Rolle spielt (wenn sie das beobachtbare Verhalten von nicht ändert) das Programm). Diese Berechtigung wird von der Als-Wenn-Regel in ISO C++ erteilt. Dies ist auch die allgemeine Klausel, die die üblichen Optimierungen ermöglicht (und es gibt auch in ISO C fast die gleiche Regel). Neben der As-If-Regel hat ISO C++ auch Kopierentscheidungsregeln , um das Weglassen bestimmter Objekterstellungen zu ermöglichen. Dadurch entfallen die beteiligten Konstruktor- und Destruktor-Aufrufe. Infolgedessen werden die automatischen Objekte (falls vorhanden) in diesen Konstruktoren und Destruktoren ebenfalls eliminiert, verglichen mit der naiven abstrakten Semantik, die der Quellcode impliziert.

Auf der anderen Seite ist die kostenlose Speicherzuweisung definitiv eine "Zuweisung" von Natur aus. Unter ISO C++ Regeln kann eine solche Zuordnung durch einen Aufruf einer Zuordnungsfunktion erreicht werden. Seit ISO C++ 14 gibt es jedoch eine neue (nicht als-wenn) Regel , um das Zusammenführen globaler Zuweisungsfunktionen (d. H. ::operator new) In bestimmten Fällen zu ermöglichen. Daher können Teile von dynamischen Zuweisungsoperationen auch nicht ausgeführt werden, wie dies bei automatischen Objekten der Fall ist.

Zuweisungsfunktionen weisen Speicherressourcen zu. Objekte können basierend auf der Zuordnung mithilfe von Zuordnern weiter zugeordnet werden. Bei automatischen Objekten werden sie direkt dargestellt - obwohl auf den zugrunde liegenden Speicher zugegriffen werden kann und dieser verwendet werden kann, um Speicher für andere Objekte bereitzustellen (durch Platzieren von new), ist dies jedoch für den freien Speicher nicht sehr sinnvoll, da dort Es gibt keine Möglichkeit, die Ressourcen an einen anderen Ort zu verschieben.

Alle anderen Bedenken fallen nicht in den Anwendungsbereich von C++. Trotzdem können sie immer noch von Bedeutung sein.

Informationen zu Implementierungen von C++

C++ macht keine reifizierten Aktivierungsdatensätze oder irgendeine Art von erstklassigen Fortsetzungen verfügbar (z. B. durch das berühmte call/cc ), es gibt keine Möglichkeit, die Aktivierungsdatensatzrahmen direkt zu manipulieren - wo die Implementierung müssen die automatischen Objekte zu platzieren. Sobald es keine (nicht portierbaren) Interoperationen mit der zugrunde liegenden Implementierung gibt ("nativer" nicht portierbarer Code, wie Inline-Assembly-Code), kann ein Weglassen der zugrunde liegenden Zuordnung der Frames ziemlich trivial sein. Wenn zum Beispiel die aufgerufene Funktion inline ist, können die Frames effektiv in andere zusammengeführt werden, so dass es keine Möglichkeit gibt, anzuzeigen, was die "Zuordnung" ist.

Sobald jedoch die Interops eingehalten werden, werden die Dinge immer komplexer. Eine typische Implementierung von C++ wird die Fähigkeit von Interop auf ISA (Befehlssatzarchitektur) mit einigen Aufrufkonventionen als die mit dem native ( ISA-Level-Maschinencode. Dies wäre insbesondere bei der Verwaltung des Stapelzeigers, der häufig direkt von einem ISA-Level-Register gehalten wird (mit wahrscheinlich spezifischen Maschinenanweisungen für den Zugriff), explizit kostspielig. Der Stapelzeiger gibt die Grenze des oberen Frames des (derzeit aktiven) Funktionsaufrufs an. Wenn ein Funktionsaufruf eingegeben wird, wird ein neuer Rahmen benötigt und der Stapelzeiger wird (abhängig von der ISA-Konvention) um einen Wert addiert oder subtrahiert, der nicht kleiner als die erforderliche Rahmengröße ist. Dem Frame wird dann zugeordnet , wenn der Stapelzeiger nach den Operationen steht. Parameter von Funktionen können ebenfalls an den Stapelrahmen übergeben werden, abhängig von der für den Aufruf verwendeten Aufrufkonvention. Der Frame kann den Speicher für automatische Objekte (wahrscheinlich einschließlich der Parameter) enthalten, die im C++ - Quellcode angegeben sind. Im Sinne solcher Implementierungen werden diese Objekte "zugeordnet". Wenn das Steuerelement den Funktionsaufruf verlässt, wird der Frame nicht mehr benötigt. In der Regel wird er freigegeben, indem der Stapelzeiger in den Zustand vor dem Aufruf zurückgesetzt wird (zuvor gemäß der Aufrufkonvention gespeichert). Dies kann als "Freigabe" angesehen werden. Diese Operationen machen den Aktivierungsdatensatz effektiv zu einer LIFO Datenstruktur, daher wird er oft " der (Aufruf-) Stapel " genannt. Der Stapelzeiger zeigt effektiv die oberste Position von an der Stapel.

Da die meisten C++ - Implementierungen (insbesondere diejenigen, die auf systemeigenen Code auf ISA-Ebene abzielen und die Assemblersprache als unmittelbare Ausgabe verwenden) ähnliche Strategien wie diese verwenden, ist ein derart verwirrendes "Zuweisungsschema" beliebt. Solche Zuweisungen (wie auch Freigabezuweisungen) verbrauchen Maschinenzyklen, und es kann teuer werden, wenn die (nicht optimierten) Aufrufe häufig auftreten, obwohl moderne CPU-Mikroarchitekturen komplexe Optimierungen aufweisen können, die von der Hardware für das allgemeine Codemuster implementiert werden (wie die Verwendung von a Stack Engine bei der Implementierung von Push/POP Anweisungen).

Im Allgemeinen ist es jedoch richtig, dass die Kosten für die Stack-Frame-Zuweisung erheblich geringer sind als bei einem Aufruf einer Zuweisungsfunktion, die den Free Store betreibt (sofern sie nicht vollständig optimiert wurde) , der selbst Hunderte (wenn nicht Millionen von :-) Operationen haben kann, um den Stapelzeiger und andere Zustände aufrechtzuerhalten. Zuweisungsfunktionen basieren in der Regel auf der API, die von der gehosteten Umgebung bereitgestellt wird (z. B. die vom Betriebssystem bereitgestellte Laufzeit). Anders als beim Halten von automatischen Objekten für Funktionsaufrufe werden solche Zuweisungen allgemein verwendet, sodass sie keine Rahmenstruktur wie ein Stapel haben. Traditionell weisen sie Speicherplatz aus dem Poolspeicher zu, der Heap (oder mehrere Heaps) genannt wird. Anders als beim "Stack" gibt der Begriff "Heap" hier nicht die verwendete Datenstruktur an. es ist von frühen Sprachimplementierungen vor Jahrzehnten abgeleitet . (Übrigens wird der Aufrufstapel normalerweise von der Umgebung beim Programm- oder Thread-Start mit einer festen oder benutzerdefinierten Größe aus dem Heap zugewiesen.) Aufgrund der Art der Anwendungsfälle sind Zuweisungen und Freigabezuweisungen aus einem Heap weitaus komplizierter (als Push oder Pop-of) Stack-Frames) und kaum durch Hardware direkt zu optimieren.

Auswirkungen auf den Speicherzugriff

Bei der üblichen Stapelzuweisung wird der neue Frame immer oben platziert, sodass er eine recht gute Lokalität aufweist. Dies ist freundlich zu zwischenspeichern. OTOH, Speicher, der zufällig im freien Speicher zugewiesen wird, hat keine solche Eigenschaft. Seit ISO C++ 17 gibt es Poolressourcenvorlagen, die von <memory> Bereitgestellt werden. Der direkte Zweck einer solchen Schnittstelle besteht darin, zuzulassen, dass die Ergebnisse aufeinanderfolgender Zuordnungen im Speicher nahe beieinander liegen. Dies erkennt die Tatsache an, dass diese Strategie im Allgemeinen für die Leistung bei zeitgemäßen Implementierungen gut ist, z. Freundlich sein, in modernen Architekturen zwischenzuspeichern. Hier geht es jedoch um die Leistung von Zugriff und nicht um Zuweisung.

Parallelität

Die Erwartung eines gleichzeitigen Speicherzugriffs kann unterschiedliche Auswirkungen auf den Stack und die Heaps haben. Ein Aufrufstapel gehört in einer C++ - Implementierung normalerweise ausschließlich einem Ausführungsthread. OTOH, Haufen werden oft shared zwischen den Threads in einem Prozess. Für solche Heaps müssen die Zuweisungs- und Freigabefunktionen die gemeinsam genutzte interne Verwaltungsdatenstruktur vor Datenrassen schützen. Infolgedessen können Heap-Zuweisungen und -Deallocations aufgrund interner Synchronisierungsvorgänge zusätzlichen Overhead verursachen.

Raumeffizienz

Aufgrund der Art der Anwendungsfälle und der internen Datenstrukturen kann es bei Heaps zu einer internen Speicherfragmentierung kommen, während dies beim Stack nicht der Fall ist. Dies hat keine direkten Auswirkungen auf die Leistung der Speicherzuweisung. In einem System mit virtuellem Speicher kann jedoch eine geringe Speichereffizienz die Gesamtleistung des Speicherzugriffs beeinträchtigen. Dies ist besonders schlimm, wenn die Festplatte als Austausch des physischen Speichers verwendet wird. Dies kann zu einer recht langen Latenz führen - manchmal zu Milliarden von Zyklen.

Einschränkungen der Stapelzuordnungen

Obwohl Stapelzuweisungen in der Leistung oftmals besser sind als Heapzuweisungen in der Realität, bedeutet dies sicherlich nicht, dass Stapelzuweisungen immer Heapzuweisungen ersetzen können.

Erstens gibt es keine Möglichkeit, Speicherplatz auf dem Stapel mit einer zur Laufzeit angegebenen Größe portabel mit ISO C++ zuzuweisen. Es gibt Erweiterungen, die von Implementierungen wie alloca und G ++ 's VLA (Array variabler Länge) bereitgestellt werden, aber es gibt Gründe, sie zu vermeiden. (IIRC, Linux-Quelle entfernt kürzlich die Verwendung von VLA.) (Beachten Sie auch, dass ISO C99 VLA vorgeschrieben hat, ISO C11 die Unterstützung jedoch optional macht.)

Zweitens gibt es keinen zuverlässigen und tragbaren Weg, um die Erschöpfung des Stapelraums zu erkennen. Dies wird häufig als Stapelüberlauf bezeichnet (hmm, die Etymologie dieser Seite), aber wahrscheinlich genauer: Stack Overrun. In der Realität führt dies häufig zu einem ungültigen Speicherzugriff und der Status des Programms ist dann beschädigt (... oder schlimmer noch, eine Sicherheitslücke). Tatsächlich hat ISO C++ kein Konzept für "den Stapel" und macht es undefiniert, wenn die Ressource erschöpft ist . Seien Sie vorsichtig, wie viel Platz für automatische Objekte übrig bleiben soll.

Wenn der Stapelspeicherplatz erschöpft ist, sind zu viele Objekte im Stapel zugeordnet. Dies kann durch zu viele aktive Funktionsaufrufe oder die nicht ordnungsgemäße Verwendung von automatischen Objekten verursacht werden. Solche Fälle können auf das Vorhandensein von Fehlern hinweisen, z. ein rekursiver Funktionsaufruf ohne korrekte Exit-Bedingungen.

Trotzdem sind manchmal tiefe rekursive Aufrufe erwünscht. In Implementierungen von Sprachen, die die Unterstützung von ungebundenen aktiven Anrufen erfordern (wobei die Anruftiefe nur durch den Gesamtspeicher begrenzt ist), ist es unmöglich, den (zeitgemäßen) nativen Anrufstapel direkt als Zielsprachaktivierungsdatensatz zu verwenden wie typische C++ Implementierungen. Um das Problem zu umgehen, sind alternative Methoden zum Erstellen von Aktivierungsdatensätzen erforderlich. Beispiel: SML/NJ weist Frames auf dem Heap explizit zu und verwendet Kaktusstapel . Die komplizierte Zuordnung solcher Aktivierungsdatensatzrahmen ist normalerweise nicht so schnell wie die Aufrufstapelrahmen. Wenn solche Sprachen jedoch mit der Garantie ordnungsgemäße Schwanzrekursion weiter implementiert werden, wird die direkte Stapelzuordnung in der Objektsprache (dh das "Objekt" in der Sprache wird nicht als Referenz gespeichert, sondern als native Primitive Werte, die eins zu eins auf nicht gemeinsam genutzte C++ - Objekte abgebildet werden können, sind noch komplizierter, da die Leistung im Allgemeinen verschlechtert wird. Bei Verwendung von C++ zur Implementierung solcher Sprachen ist es schwierig, die Auswirkungen auf die Leistung abzuschätzen.

3
FrankHB

Die Stapelzuweisung besteht aus mehreren Befehlen, während der schnellste mir bekannte RTOS-Heap-Zuweiser (TLSF) im Durchschnitt 150 Befehle verwendet. Stapelzuweisungen erfordern auch keine Sperre, da sie Thread-lokalen Speicher verwenden, was ein weiterer großer Leistungsgewinn ist. Die Stapelzuweisungen können also 2-3 Größenordnungen schneller sein, je nachdem, wie stark Ihre Umgebung mit mehreren Threads ausgelastet ist.

Im Allgemeinen ist die Heap-Zuweisung Ihr letzter Ausweg, wenn Sie Wert auf Leistung legen. Eine praktikable Zwischenoption kann ein fester Pool-Allokator sein, bei dem es sich ebenfalls nur um ein paar Anweisungen handelt und der nur einen sehr geringen Overhead pro Allokation hat, sodass er sich hervorragend für kleine Objekte mit fester Größe eignet. Auf der anderen Seite funktioniert es nur mit Objekten mit fester Größe, ist nicht inhärent threadsicher und weist Blockfragmentierungsprobleme auf.

3

Es gibt einen allgemeinen Grund für solche Optimierungen.

Die Optimierung, die Sie erhalten, ist proportional zu der Zeit, die der Programmzähler tatsächlich in diesem Code ist.

Wenn Sie den Programmzähler abtasten, werden Sie feststellen, wo er seine Zeit verbringt, und das ist normalerweise ein winziger Teil des Codes, und häufig in Bibliotheksroutinen, über die Sie keine Kontrolle haben.

Nur wenn Sie feststellen, dass die Heap-Zuordnung Ihrer Objekte viel Zeit in Anspruch nimmt, können Sie sie merklich schneller stapeln.

2
Mike Dunlavey

Wie andere gesagt haben, ist die Stapelzuweisung im Allgemeinen viel schneller.

Wenn es jedoch teuer ist, Ihre Objekte zu kopieren, kann die Zuweisung auf dem Stapel zu einem großen Leistungseinbruch führen, wenn Sie die Objekte später verwenden, wenn Sie nicht vorsichtig sind.

Wenn Sie beispielsweise etwas auf dem Stapel zuweisen und es dann in einen Container legen, wäre es besser gewesen, es auf dem Heap zuzuweisen und den Zeiger im Container zu speichern (z. B. mit einem std :: shared_ptr <>). Dasselbe gilt, wenn Sie Objekte nach Wert übergeben oder zurückgeben, und in anderen ähnlichen Szenarien.

Der Punkt ist, dass, obwohl die Stapelzuweisung in vielen Fällen besser ist als die Heapzuweisung, manchmal mehr Probleme verursachen kann, als gelöst werden, wenn Sie sich beim Stapeln abmühen, wenn es nicht am besten zum Berechnungsmodell passt.

2
wjl
class Foo {
public:
    Foo(int a) {

    }
}
int func() {
    int a1, a2;
    std::cin >> a1;
    std::cin >> a2;

    Foo f1(a1);
    __asm Push a1;
    __asm lea ecx, [this];
    __asm call Foo::Foo(int);

    Foo* f2 = new Foo(a2);
    __asm Push sizeof(Foo);
    __asm call operator new;//there's a lot instruction here(depends on system)
    __asm Push a2;
    __asm call Foo::Foo(int);

    delete f2;
}

Es wäre so in asm. Wenn Sie sich in func befinden, wurden der f1 Und der Zeiger f2 Im Stapel (automatisierter Speicher) zugeordnet. Übrigens hat Foo f1(a1) keine Anweisungseffekte auf den Stapelzeiger (esp). Es wurde zugewiesen, wenn func das Mitglied f1, die Anweisung sieht ungefähr so ​​aus: lea ecx [ebp+f1], call Foo::SomeFunc(). Eine andere Sache, die der Stack zuweist, kann jemanden denken lassen, der Speicher sei so etwas wie FIFO, das FIFO ist gerade passiert, als Sie in eine Funktion gegangen sind, wenn Sie in der Funktion sind und etwas wie int i = 0, Da ist kein Push passiert.

2
bitnick

Es wurde bereits erwähnt, dass die Stapelzuweisung lediglich den Stapelzeiger bewegt, d. H. Einen einzelnen Befehl für die meisten Architekturen. Vergleichen Sie das mit dem, was im Allgemeinen bei der Heap-Zuweisung passiert.

Das Betriebssystem verwaltet Teile des freien Speichers als verknüpfte Liste mit den Nutzdaten, die aus dem Zeiger auf die Startadresse des freien Teils und der Größe des freien Teils bestehen. Um X Speicherbytes zuzuweisen, wird die Verknüpfungsliste durchlaufen und jede Note nacheinander aufgesucht, um festzustellen, ob ihre Größe mindestens X beträgt. Wenn ein Teil mit der Größe P> = X gefunden wird, wird P mit in zwei Teile geteilt Größen X und PX. Die verknüpfte Liste wird aktualisiert und der Zeiger auf den ersten Teil wird zurückgegeben.

Wie Sie sehen können, hängt die Heap-Zuweisung von Faktoren ab, wie viel Speicher Sie anfordern, wie fragmentiert der Speicher ist und so weiter.

1
Nikhil

Im Allgemeinen ist die Stapelzuweisung schneller als die Heapzuweisung, wie in fast jeder Antwort oben erwähnt. Ein Stack-Push oder -Pop ist O (1), wohingegen das Zuweisen oder Freigeben eines Heaps ein Durchlaufen früherer Zuweisungen erfordern kann. Normalerweise sollten Sie jedoch keine engen, leistungsintensiven Schleifen verwenden, damit die Auswahl in der Regel auf andere Faktoren beschränkt bleibt.

Es könnte sinnvoll sein, diese Unterscheidung zu treffen: Sie können einen "Stapelzuweiser" auf dem Heap verwenden. Streng genommen verstehe ich unter Stapelzuweisung eher die tatsächliche Zuweisungsmethode als den Ort der Zuweisung. Wenn Sie eine Menge Dinge auf dem eigentlichen Programmstapel zuordnen, kann dies aus verschiedenen Gründen schlimm sein. Auf der anderen Seite ist die Verwendung einer Stapelmethode zum Zuweisen auf dem Heap, wenn dies möglich ist, die beste Wahl, die Sie für eine Zuweisungsmethode treffen können.

Da Sie Metrowerks und PPC erwähnt haben, meine ich wohl Wii. In diesem Fall ist der Speicher knapp und die Verwendung einer Stapelzuweisungsmethode, wo immer dies möglich ist, garantiert, dass Sie keinen Speicher für Fragmente verschwenden. Dies erfordert natürlich viel mehr Sorgfalt als "normale" Heap-Zuweisungsmethoden. Es ist ratsam, die Kompromisse für jede Situation zu bewerten.

1
Dan Olson

Beachten Sie, dass es bei den Überlegungen in der Regel nicht um Geschwindigkeit und Leistung geht, wenn Sie Stapel oder Heap zuweisen. Der Stapel verhält sich wie ein Stapel, was bedeutet, dass er gut geeignet ist, um Blöcke zu schieben und sie wieder zu platzieren. Die Ausführung von Prozeduren erfolgt ebenfalls stapelartig. Die zuletzt eingegebene Prozedur muss zuerst beendet werden. In den meisten Programmiersprachen sind alle Variablen, die in einer Prozedur benötigt werden, nur während der Ausführung der Prozedur sichtbar. Sie werden daher beim Eintreten in eine Prozedur verschoben und beim Beenden oder Zurückkehren aus dem Stapel entfernt.

Nun zu einem Beispiel, in dem der Stack nicht verwendet werden kann:

Proc P
{
  pointer x;
  Proc S
  {
    pointer y;
    y = allocate_some_data();
    x = y;
  }
}

Wenn Sie in Prozedur S Speicher zuweisen und auf den Stapel legen und dann S beenden, werden die zugewiesenen Daten aus dem Stapel entfernt. Die Variable x in P zeigte jedoch auch auf diese Daten, sodass x nun auf eine Stelle unter dem Stapelzeiger zeigt (vorausgesetzt, der Stapel wächst nach unten), deren Inhalt unbekannt ist. Der Inhalt ist möglicherweise noch vorhanden, wenn der Stapelzeiger nur nach oben verschoben wird, ohne die Daten darunter zu löschen. Wenn Sie jedoch neue Daten auf dem Stapel zuweisen, zeigt der Zeiger x möglicherweise stattdessen auf diese neuen Daten.

Gehen Sie niemals von einer vorzeitigen Annahme aus, da der Code und die Verwendung anderer Anwendungen Ihre Funktion beeinträchtigen können. Wenn man also die Funktion betrachtet, ist Isolation sinnlos.

Wenn Sie es mit Anwendungen ernst meinen, tun Sie dies mit VTune, oder verwenden Sie ein ähnliches Profilierungswerkzeug, und schauen Sie sich Hotspots an.

Ketan

0
Ketan