2016-10-14 4 views
3

Betrachten Sie folgenden Code:Wie ermittelt der Compiler die benötigte Stackgröße für eine Funktion mit vom Compiler generierten Provisorien?

class cFoo { 
    private: 
     int m1; 
     char m2; 
    public: 
     int doSomething1(); 
     int doSomething2(); 
     int doSomething3(); 
} 

class cBar { 
    private: 
     cFoo mFoo; 
    public: 
     cFoo getFoo(){ return mFoo; } 
} 

void some_function_in_the_callstack_hierarchy(cBar aBar) { 
    int test1 = aBar.getFoo().doSomething1(); 
    int test2 = aBar.getFoo().doSomething2(); 
    ... 
} 

In der Zeile, in getFoo() der Compiler ein temporäres Objekt von cFoo generiert genannt wird, doSomething1 nennen zu können(). Verwendet der Compiler den Stapelspeicher, der für diese temporären Objekte verwendet wird? Wie viele Stapelspeicher wird der Aufruf von "some_function_in_the_callstack_hierarchy" reservieren? Speichert es Speicher für jedes generierte temporäre Objekt?

Meine Vermutung war, dass der Compiler nur Erinnerung an cFoo für ein Objekt reservieren und den Speicher für verschiedene Anrufe wiederverwenden, aber wenn ich

int test3 = aBar.getFoo().doSomething3(); 

hinzufügen kann ich sehen, dass die benötigte Stack-Größe für „some_function_in_the_callstack_hierarchy“ ist viel mehr und es ist nicht nur wegen der zusätzlichen lokalen int-Variable.

Auf der anderen Seite, wenn ich dann

cFoo getFoo(){ return mFoo; } 

mit einem Referenz ersetzen (nur für einen bestimmten Zweck zu testen, weil ein Verweis auf ein privates Mitglied Rückkehr ist nicht gut)

const cFoo& getFoo(){ return mFoo; } 

es braucht Weg weniger Stapelspeicher, als die Größe eines cFoo.

Also für mich scheint es, dass der Compiler zusätzlichen Stack-Speicher für jedes generierte temporäre Objekt in der Funktion reserviert. Aber das wäre sehr ineffizient. Kann jemand das erklären?

+3

Es ist ziemlich Implementierung-definiert. Es ist nicht einmal garantiert, dass Objekte mit automatischem Speicher überhaupt auf dem Stapel erstellt werden. – SingerOfTheFall

+1

Nur für den Fall, dass Sie nicht sicherstellen, dass Sie mit etwas wie "-O2" aktiviert kompilieren. Das Analysieren eines nicht optimierten Builds ist nicht sehr hilfreich. – NathanOliver

+0

Es ist kompiliert mit -O1 – Matthias

Antwort

4

Die optimizing compiler wandelt Ihren Quellcode in eine interne Darstellung um und normalisiert sie.

Mit free software Compiler (wie GCC & Clang/LLVM), sind Sie in der Lage in diese interne Darstellung zu sehen (zumindest durch den Compiler Code Patchen oder es in irgendeine Debugger ausgeführt wird).

Übrigens benötigen manchmal temporäre Werte nicht einmal einen Stapelspeicherplatz, z.B. weil sie optimiert wurden oder weil sie in Registern sitzen können. Und ziemlich oft würden sie einen nicht benötigten Platz im aktuellen Anrufrahmen wiederverwenden. Auch (vor allem in C++) sind viele (kleine) Funktionen inlined -wie Ihre getFoo wahrscheinlich- (also haben sie selbst keinen Call-Frame). Neuere GCC sind sogar manchmal in der Lage von tail-call Optimierungen (im Wesentlichen, Wiederverwendung der Call-Frame des Anrufers).

Wenn Sie mit GCC kompilieren (d. H. g++) würde ich vorschlagen, mit optimization options und developer options (und einige andere) zu spielen. verwenden Vielleicht also-Wstack-usage=48 (oder einen anderen Wert in Byte pro Aufruf Frame) und/oder -fstack-usage

Erstens, wenn Sie Assembler-Code lesen können, kompilieren yourcode.cc mit g++ -S -fverbose-asm -O yourcode.cc und schauen in die emittierte yourcode.s

(don Vergessen Sie nicht, mit Optimierungsflags zu spielen, also ersetzen Sie -O durch -O2 oder -O3 ....)

Dann, wenn Sie mehr gespannt, wie der Compiler sind optimiert, versuchen g++ -O -fdump-tree-all -c yourcode.cc und Sie werden eine Menge von sogenannten „Dump-Dateien“, die eine Teil textliche Wiedergabe von internen Repräsentationen relevant GCC enthalten bekommen .

Wenn Sie noch neugieriger sind, schauen Sie in meine GCC MELT und insbesondere seine documentation Seite (die eine viel von Dias & Referenzen enthält).

Für mich scheint es, dass der Compiler zusätzlichen Stack-Speicher für jedes generierte temporäre Objekt in der Funktion reserviert.

Sicher nicht, im allgemeinen Fall (und natürlich vorausgesetzt, Sie aktivieren einige Optimierungen). Und selbst wenn etwas Platz reserviert ist, würde er sehr schnell wiederverwendet werden.

BTW: Beachten Sie, dass der C++ 11-Standard nicht von Stack spricht. Man könnte sich ein C++ - Programm vorstellen, das kompiliert wurde, ohne einen Stack zu verwenden (zB eine ganze Programmoptimierung, die ein Programm ohne Rekursion aufspürt, dessen Stack und Layout optimiert werden könnten, um jeden Stack zu vermeiden. Ich kenne diese Compiler nicht kann ziemlich clever sein ....)

4

Versuch zu analysieren wie ein Compiler wird ein bestimmtes Stück Code behandeln wird zunehmend schwieriger als Optimierung Strategien werden aggressiver.

Alles, was ein Compiler tun muss, ist den C++ - Standard zu implementieren und den Code zu kompilieren, ohne irgendwelche Nebeneffekte einzuführen oder aufzuheben (mit einigen Ausnahmen wie Rückgabe und benannter Rückgabewertoptimierung).

Sie können von Ihrem Code sehen, dass, da cFoo keine polymorpher Typ ist und keine Mitgliederdaten, ein Compiler insgesamt die Erzeugung eines Objekts optimieren konnte und rufen, was im Wesentlichen deshalb direkt static Funktionen. Ich könnte mir vorstellen, dass einige Compiler dies bereits zum Zeitpunkt meiner Arbeit tun. Sie können immer die Ausgabebaugruppe überprüfen, um sicher zu sein.

Edit: Das OP hat jetzt Klassenmitglieder eingeführt. Aber da diese nie initialisiert werden und private sind, kann der Compiler sie entfernen, ohne zu viel darüber nachzudenken. Diese Antwort gilt daher immer noch.

+0

Ich habe vergessen, einige Mitglieder zu cFoo hinzuzufügen. Ich habe die Frage bearbeitet, so dass eine Instanz von cFoo Speicher für die Mitgliedsvariablen benötigt. – Matthias

+0

Danke für die Ungültigkeit meiner Antwort ;-). Es gilt immer noch, da die Variablen niemals initialisiert werden. – Bathsheba

+0

Das wäre nur wahr, wenn sie nie benutzt werden.Aber sie werden in der DoSomething-Funktion verwendet. Beachten Sie, dass dieser Code nur für die Frage gilt. Ich kann Ihnen meinen wirklichen Code nicht zeigen, weil er meinem Arbeitgeber gehört – Matthias

1

Die Lebensdauer eines temporären Objekts ist bis zum Ende des vollständigen enthaltenden Ausdrucks, siehe Abschnitt "12.2 Temporäre Objekte" des Standards.

Es ist sehr unwahrscheinlich, dass selbst bei den niedrigsten Optimierungseinstellungen ein Compiler den Speicherplatz nach dem Ende der Lebensdauer eines temporären Objekts nicht wiederverwenden wird.

Verwandte Themen