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 der Hobgoblin der kleinen Köpfe ist . Anscheinend sind optimierende Compiler die Hobgoblins vieler Programmierer. Diese Diskussion stand früher am Ende der Antwort, aber die Leute können sich anscheinend nicht die Mühe machen, so weit zu lesen. Deshalb verschiebe ich sie hierher, um zu vermeiden, dass ich Fragen bekomme, die ich bereits beantwortet habe.
Ein optimierender Compiler stellt möglicherweise fest, dass dieser Code nichts bewirkt, und optimiert möglicherweise alles weg. Es ist die Aufgabe des Optimierers, solche Dinge zu tun, und der Kampf gegen den Optimierer ist ein Kinderspiel.
Ich würde empfehlen, diesen Code bei deaktivierter Optimierung zu kompilieren, da es keine gute Möglichkeit gibt, jeden derzeit verwendeten oder in Zukunft verwendeten Optimierer zu täuschen.
Jeder, der den Optimierer einschaltet und sich dann über dessen Bekämpfung beschwert, sollte öffentlich lächerlich gemacht werden.
Wenn ich mich um Nanosekundengenauigkeit kümmern würde, würde ich sie nicht verwenden std::clock()
. Wenn ich die Ergebnisse als Doktorarbeit veröffentlichen wollte, würde ich eine größere Sache darüber machen und wahrscheinlich GCC, Tendra / Ten15, LLVM, Watcom, Borland, Visual C ++, Digital Mars, ICC und andere Compiler vergleichen. So wie es ist, dauert die Heap-Zuweisung hunderte Male länger als die Stapelzuweisung, und ich sehe nichts Nützliches darin, die Frage weiter zu untersuchen.
Der Optimierer hat die Mission, den Code, den ich teste, loszuwerden. Ich sehe keinen Grund, dem Optimierer zu sagen, dass er ausgeführt werden soll, und dann zu versuchen, den Optimierer dazu zu bringen, nicht wirklich zu optimieren. Aber wenn ich Wert darin sehen würde, würde ich eine oder mehrere der folgenden Aktionen ausführen:
Fügen Sie ein Datenelement hinzu empty
und greifen Sie auf dieses Datenelement in der Schleife zu. Wenn ich jedoch immer nur aus dem Datenelement lese, kann der Optimierer ständig falten und die Schleife entfernen. Wenn ich immer nur in das Datenelement schreibe, überspringt der Optimierer möglicherweise alle bis auf die letzte Iteration der Schleife. Außerdem lautete die Frage nicht "Stapelzuweisung und Datenzugriff vs. Heapzuweisung und Datenzugriff".
Deklarieren e
volatile
, wird aber volatile
oft falsch kompiliert (PDF).
Nehmen Sie die Adresse e
innerhalb der Schleife (und weisen Sie sie möglicherweise einer Variablen zu, extern
die in einer anderen Datei deklariert und definiert ist). Aber selbst in diesem Fall kann der Compiler feststellen, dass - zumindest auf dem Stapel - e
immer dieselbe Speicheradresse zugewiesen wird, und dann wie in (1) oben konstant gefaltet wird. Ich bekomme alle Iterationen der Schleife, aber das Objekt wird nie wirklich zugewiesen.
Über das Offensichtliche hinaus ist dieser Test insofern fehlerhaft, als er sowohl die Zuweisung als auch die Freigabe misst, und die ursprüngliche Frage hat nicht nach der Freigabe gefragt. Natürlich werden auf dem Stapel zugewiesene Variablen am Ende ihres Gültigkeitsbereichs automatisch freigegeben, sodass ein Nichtaufruf delete
(1) die Zahlen verzerren würde (die Freigabe des Stapels ist in den Zahlen zur Stapelzuweisung enthalten, daher ist es nur fair, die Freigabe des Heapspeichers zu messen) und ( 2) verursachen einen ziemlich schlechten Speicherverlust, es sei denn, wir behalten einen Verweis auf den neuen Zeiger und rufen auf, delete
nachdem wir unsere Zeitmessung erhalten haben.
Auf meinem Computer, der g ++ 3.4.4 unter Windows verwendet, erhalte ich "0 Clock Ticks" für die Stapel- und Heap-Zuordnung für weniger als 100000 Zuweisungen, und selbst dann erhalte ich "0 Clock Ticks" für die Stack-Zuweisung und "15 Clock Ticks" "für die Heap-Zuordnung. Wenn ich 10.000.000 Zuweisungen messe, benötigt die Stapelzuweisung 31 Takt-Ticks und die Heap-Zuweisung 1562 Takt-Ticks.
Ja, ein optimierender Compiler kann das Erstellen der leeren Objekte vermeiden. Wenn ich es richtig verstehe, kann es sogar die gesamte erste Schleife auslassen. Als ich die Iterationen auf 10.000.000 stapelte, dauerte die Stapelzuweisung 31 Takt-Ticks und die Heap-Zuweisung 1562 Takt-Ticks. Ich denke, man kann mit Sicherheit sagen, dass g ++ die Konstruktoren nicht entfernt hat, ohne g ++ anzuweisen, die ausführbare Datei zu optimieren.
In den Jahren, seit ich dies geschrieben habe, bestand die Präferenz für Stack Overflow darin, die Leistung von optimierten Builds zu veröffentlichen. Im Allgemeinen denke ich, dass dies richtig ist. Ich halte es jedoch immer noch für dumm, den Compiler zu bitten, den Code zu optimieren, wenn Sie diesen Code tatsächlich nicht optimieren möchten. Es scheint mir sehr ähnlich zu sein, als würde man für den Parkservice extra bezahlen, aber ich weigere mich, die Schlüssel zu übergeben. 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 adressieren, den das ursprüngliche Programm nicht jedes Mal durch die Schleife auf dem Stapel zugewiesen hat) und Kompilieren ohne Optimierungen, sondern Verknüpfen mit Release-Bibliotheken (um den gültigen Punkt zu adressieren, den wir nicht verwenden Ich möchte keine Verlangsamung einschließen, die durch die Verknüpfung mit Debug-Bibliotheken verursacht wird.
#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 beim Kompilieren mit der Kommandozeile cl foo.cc /Od /MT /EHsc
.
Sie sind möglicherweise nicht mit meinem Ansatz einverstanden, einen nicht optimierten Build zu erhalten. Das ist in Ordnung: Ändern Sie den Benchmark so oft Sie möchten. 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 anständige Compiler feststellen kann, dass on_stack
dies nichts Nützliches bewirkt und wegoptimiert werden kann. GCC auf meinem Linux-Laptop bemerkt auch, dass on_heap
es nichts Nützliches tut, und optimiert es auch weg:
on_stack took 0.000003 seconds
on_heap took 0.000002 seconds