Gründe für Codeaufblähung mit C++-Templates



  • Hallo,

    ich habe das Problem, dass eine Anwendung durch häufigen Einsatz der STL (hauptsächlich, aber auch Templates im Allgemeinen) von einer Executablegröße von ca. 10 auf 100MB angewachsen ist (statisch gelinkt, mit Debugsymbolen). Selbst das gestrippte Executable ist noch ~30MB groß. (zur Information, das Projekt hat ca~ 130 Module und 3000 Sourcefiles)

    Das ist natürlich nicht von heute auf morgen passiert, sondern war ein Entwicklungsprozess von mehreren Jahren. Trotzdem rechtfertigt der eigentliche Code nicht diese Executable-Größe.

    Also wurden Untersuchungen angestellt, warum das so ist. Unter anderem wird der g++ unter Linux als Compiler eingesetzt. Der Compilerbeschreibung und Einstellung nach sollte jedes Template nur ein einziges Mal im Code vorkommen. Sprich es sollte keine n-fachen Instanziierungen von vector<int>, vector<double>, etc. geben. Die erste Frage ist, wie sich das an einem Compilat feststellen lässt. nm funktioniert ja leider nur auf .a-Files. Welche Tools gibt es hier? (ELF-Format)

    Angenommen, es gibt tatsächlich keine mehrfachen Templates (ist momentan die Annahme), stellt sich die Frage, woher sonst die extreme Aufblähung kommt. Der Verdacht fällt hierbei vector<T*>-Instanziierungen, die zuhauf im Code vorkommen. Würde das für jeden Typ T* neu instanziiert, wäre das natürlich eine Erklärung. Ich habe von Ansätzen gelesen, eine Pointerspezialisierung mit einem vector<void*> im Hintergrund der STL zu benutzen. Dies soll aber wiederum nicht immer funktionieren und nicht standard-konform sein (längliche Newsgroup-Dikussion, die Quelle ist mir leider nicht vorhanden).
    Das dumme daran ist, dass alle vector<T*>-Instanziierungen sehr wahrscheinlich exakt den selben Maschinencode haben.
    Meine zweite Frage also - wenn das der Grund für die Codegröße ist, was kann man tun? Ein Refactoring kommt schon allein aufgrund der Projektgröße nicht in Frage. Die Lösung sollte schon möglichst per Compilerschalter etc. machbar sein. Ich habe mal versucht, den GNU ld mit Optimierung laufen zu lassen (in der Hoffnung, er könne per WPA Codeduplikate entfernen sofern vorhanden) - habe das ganze aber abgebrochen, nachdem nach 2 Stunden immernoch kein Executable da war. Das wäre auch keine akzepable Zeit für einen Link (ganz eventuell für's Release-Exe).

    Falls meine Denkansätze und Versuche völlig falsch sind, lasse ich mich natürlich auch gerne eines anderen belehren. 🙂

    Falls ich nur zu blöd zum Suchen war, freue ich mich auch über informative Links. 😃



  • 7H3 N4C3R schrieb:

    Angenommen, es gibt tatsächlich keine mehrfachen Templates (ist momentan die Annahme), stellt sich die Frage, woher sonst die extreme Aufblähung kommt. Der Verdacht fällt hierbei vector<T*>-Instanziierungen, die zuhauf im Code vorkommen. Würde das für jeden Typ T* neu instanziiert, wäre das natürlich eine Erklärung. Ich habe von Ansätzen gelesen, eine Pointerspezialisierung mit einem vector<void*> im Hintergrund der STL zu benutzen. Dies soll aber wiederum nicht immer funktionieren und nicht standard-konform sein (längliche Newsgroup-Dikussion, die Quelle ist mir leider nicht vorhanden).

    Ich denke, das dürfte tatsächlich der Grund für deine riesige EXE sein - auch wenn sie auf Maschinenebene identisch behandelt werden, sind int* und double* zwei völlig verschiedene Typen - und da erzeugt der Compiler zwei unabhängige Spezialisierungen.

    Meine zweite Frage also - wenn das der Grund für die Codegröße ist, was kann man tun? Ein Refactoring kommt schon allein aufgrund der Projektgröße nicht in Frage. Die Lösung sollte schon möglichst per Compilerschalter etc. machbar sein. Ich habe mal versucht, den GNU ld mit Optimierung laufen zu lassen (in der Hoffnung, er könne per WPA Codeduplikate entfernen sofern vorhanden) - habe das ganze aber abgebrochen, nachdem nach 2 Stunden immernoch kein Executable da war. Das wäre auch keine akzepable Zeit für einen Link (ganz eventuell für's Release-Exe).

    Erste Lösung wäre es natürlich, weniger Pointer(klassen) durch den Raum zu werfen. Als Alternative würde sich tatsächlich ein vector<void*> als Lösung anbieten (eventuell mit einer Wrapperklasse drumherum, um die Typsicherheit zu garantieren).

    Ob du tatsächlich einen Compiler überreden könntest, aus verschiedenen vector<T*> Klassen nur eine Spezialisierung zu generieren, wage ich jetzt zu bezweifeln.



  • Ich bezweifel mal, das jeder Compiler so schlau ist, und z.B. hunderte neue definierte std::vector<int> wegoptimiert.

    Hast du sowas in deinem Code:

    typedef std::vector<int> intVector;
    typedef std::vector<double> doubleVector;
    

    Neue Compiler haben eine WPO (Whole Program Optimazation) Funktion, die nach dem Linken nochmal das ganze Programm durch geht und optimiert. Viele Compiler (vorallem ältere) optimieren nur einzelne Objektfiles. Wenn du in zwei Objektfiles jeweils ein vector<int> template spezialisiert hast, wird wohl auch doppelter Code erzeugt werden.

    Ich schätze mal (ich habe keine Nachforschungen angestellt) das erst durch ein typedef der Compiler so schlau sein kann, und z.B. den Code für den intVector nur einmal erzeugt.

    Welche GCC-version benutzt du denn? Und vielleicht hilft es mal testhalben ein paar spezialisierungen durch einheitliche typedefs zu erstzen und zu schauen, ob der Code dadurch etwas kleiner geworden ist?

    Letztendlich muß ich sagen, hat mich die Thematik damals auch schon mal gedanklich beschäftigt. Habe aber bisher nie so große C++ Projekte gehabt, um danach intensiver zu forschen. Aber deine Frage an sich ist völlig legetim! 🙂



  • CStoll schrieb:

    Erste Lösung wäre es natürlich, weniger Pointer(klassen) durch den Raum zu werfen. Als Alternative würde sich tatsächlich ein vector<void*> als Lösung anbieten (eventuell mit einer Wrapperklasse drumherum, um die Typsicherheit zu garantieren).

    Bei wie gesagt ca. 3000 Sourcefiles kommt das leider nicht in Frage.

    @Artchi:
    WPA (Whole Program Analysis) habe ich ja schon versucht, durch die aktivierte Linker-Optimierung. Aber wie gesagt, zwei Stunden Link-Zeit? Das ist für produktives Arbeiten nicht akzeptabel. Ein typedef sollte per se nichts bringen, da es ja immernoch genau den selben Typ bezeichnet und keinen neuen erzeugt.

    Wie gesagt, ich glaube nicht, dass überall wo vector<int> benutzt wird, eine neue Instanziierung erzeugt wird. Der g++ schreibt hierfür übrigens spezielle Anweisungen für den GNU ld in die Objekt-Files. Per Schalter lassen sich diese Anweisungen auch in separate Files schreiben. (.rpo glaube ich) Diese wertet ld dann aus und sorgt dann dafür, dass jede Instanziierung nur einmal vorkommt. Ein guter Hinweis dafür ist übrigens, dass die Mangeled Names ein gnu_once enthalten. Wie schon im obigen Beitrag gefragt, wie kann ich alle Symbole einer Exedatei sehen? Bei Bibliotheken eigenet sich nm, bei Executables funktioniert der aber nicht mehr.

    Der g++ ist übrigens in Version 3.3.6, mit den dazu passenden GNU Tools.



  • 7H3 N4C3R schrieb:

    CStoll schrieb:

    Erste Lösung wäre es natürlich, weniger Pointer(klassen) durch den Raum zu werfen. Als Alternative würde sich tatsächlich ein vector<void*> als Lösung anbieten (eventuell mit einer Wrapperklasse drumherum, um die Typsicherheit zu garantieren).

    Bei wie gesagt ca. 3000 Sourcefiles kommt das leider nicht in Frage.

    3000 Sourcefiles? Woran arbeitest du denn?

    Du könntest dir natürlich eine eigene Template-Spezialisierung "template<typename T> vector<T*>" schreiben und dann überall dort einbinden, wo vector<T*> vorkommt.

    Wie schon im obigen Beitrag gefragt, wie kann ich alle Symbole einer Exedatei sehen? Bei Bibliotheken eigenet sich nm, bei Executables funktioniert der aber nicht mehr.

    Ich vermute mal, gar nicht - der Linker benötigt noch die Symbole, um zwischen den einzelnen Übersetzungseinheiten Verbindungen herzustellen, in der fertigen EXE bleibt davon nur ein jmp-Befehl übrig.



  • 7H3 N4C3R schrieb:

    @Artchi:
    WPA (Whole Program Analysis) habe ich ja schon versucht, durch die aktivierte Linker-Optimierung. Aber wie gesagt, zwei Stunden Link-Zeit? Das ist für produktives Arbeiten nicht akzeptabel.

    Das macht man ja auch nicht während des Arbeitens. Wenn du entwickelst, schaltet man ja die optimierungen aus. Wenn du ein Release für den Enduser machst, schmeisst man alle Optimierungen ein. Und das builden lässt man dann auch auf einem Build-Rechner laufen und nicht auf dem des Entwicklers. Habt ihr kein autom. Buildsystem, das zu bestimmten Zeiten einen Build startet? Z.B. alle paar Stunden immer wieder oder einmal nachts.

    Und wenn ich dich richtig verstehe, wird die Exe kleiner, wenn du WPA einschaltest?



  • CStoll schrieb:

    Du könntest dir natürlich eine eigene Template-Spezialisierung "template<typename T> vector<T*>" schreiben und dann überall dort einbinden, wo vector<T*> vorkommt.

    Das ist leider nicht standardkonform.

    Selbst bei einer Lösung die auf allen Plattformen funktioniert, müsste man immernoch einen Großteil der Sourcefiles anfassen. 😞 Ob ich sagen darf, um was für eine Anwendung es geht, weiß ich garnicht. 🙂 Geschäftsgeheimnisse und so... steht zumindest in meinem Vertrag. 🙂

    CStoll schrieb:

    Ich vermute mal, gar nicht - der Linker benötigt noch die Symbole, um zwischen den einzelnen Übersetzungseinheiten Verbindungen herzustellen, in der fertigen EXE bleibt davon nur ein jmp-Befehl übrig.

    Die Symbole sind auf jeden Fall noch da. Wie sollte sonst ein Debugger mit der Exe arbeiten. 😉 Auf jeden Fall bei einem Debug-Executable.



  • Artchi schrieb:

    Das macht man ja auch nicht während des Arbeitens. Wenn du entwickelst, schaltet man ja die optimierungen aus. Wenn du ein Release für den Enduser machst, schmeisst man alle Optimierungen ein. Und das builden lässt man dann auch auf einem Build-Rechner laufen und nicht auf dem des Entwicklers. Habt ihr kein autom. Buildsystem, das zu bestimmten Zeiten einen Build startet? Z.B. alle paar Stunden immer wieder oder einmal nachts.

    Ein Buildsystem haben wir selbstverständlich. Nur die Tester arbeiten auf Release-Executables. Wenn die erst Mittags oder Abends im Objektworkspace stehen, wird es etwas unangenehm. Und nur für die Auslieferung eines bis dato ungetesteten Schalter zu aktivieren wäre viel zu riskant.

    Artchi schrieb:

    Und wenn ich dich richtig verstehe, wird die Exe kleiner, wenn du WPA einschaltest?

    7H3 N4C3R schrieb:

    Ich habe mal versucht, den GNU ld mit Optimierung laufen zu lassen (in der Hoffnung, er könne per WPA Codeduplikate entfernen sofern vorhanden) - habe das ganze aber abgebrochen, nachdem nach 2 Stunden immernoch kein Executable da war. Das wäre auch keine akzepable Zeit für einen Link (ganz eventuell für's Release-Exe).



    • nm kann man auch auf ausführbare Programme anwenden. Wenn die Symbole gestrippt sind, bekommt man nur noch die dynamischen mit nm -D
    • Doppelte Instanzierungen, wie sie über mehrere Übersetzungseinheiten hinweg entstehen können sind erstmal doppelt vorhanden. Die Symbole sind aber weak. Der Linker nimmt dann nur eines für die fertige Datei.


  • Versuch mal das folgende Programm auf dein Binary anzuwenden:

    http://zigzag.cs.msu.su/~ghost/blog_data/function_duplicates.cpp

    Es kommt aus folgendem Beitrag:

    http://vladimir_prus.blogspot.com/2005/03/duplicate-function-bodies.html

    Ich hab mal damit rumgespielt und das Größte was ich gesehen habe war eine Einsparung von ungefähr 4000 Bytes bei einer Lib von uns, die sehr viel Templates nutzt.



  • Dankeschön, das werde ich mal ausprobieren. Erfolgsrückmeldung kommt noch.

    Der Beitrag, bzw. auch der Kommentar im Blog klingt aber eher ernüchternd. 😞



  • nm --demangle | uniq -D | wc -l

    sagt gerade mal 22. 😞

    In der Ausgabe sieht man aber in der Tat zigtausende Instanziierungen von vector, construct, destroy und vieles mehr.

    Dem Artikel von ponto nach ist es wohl auch verboten diese zu eliminieren, wenn man C++-standardkonform sein will - selbst wenn die Instanziierungen den selben Maschinencode haben. Denn: Die Adresse eines Members muss für verschiedene Typen immer verschieden sein. Also z.B.

    template <typename T> void dummy() {}
    
    int main()
    {
      void( *p1)() = dummy<void*>;
      void( *p2)() = dummy<int*>;
      assert( p1 != p2);
    }
    

    Da schaut's wohl schlecht aus mit automatischer Optimierung. 😞 Evtl. doch void*-Vectoren mit einer Thunk-Klasse davor.



  • 7H3 N4C3R schrieb:

    nm --demangle | uniq -D | wc -l

    sagt gerade mal 22. 😞

    In der Ausgabe sieht man aber in der Tat zigtausende Instanziierungen von vector, construct, destroy und vieles mehr.

    Dem Artikel von ponto nach ist es wohl auch verboten diese zu eliminieren, wenn man C++-standardkonform sein will - selbst wenn die Instanziierungen den selben Maschinencode haben. Denn: Die Adresse eines Members muss für verschiedene Typen immer verschieden sein. Also z.B.

    template <typename T> void dummy() {}
    
    int main()
    {
      void( *p1)() = dummy<void*>;
      void( *p2)() = dummy<int*>;
      assert( p1 != p2);
    }
    

    Da schaut's wohl schlecht aus mit automatischer Optimierung. 😞 Evtl. doch void*-Vectoren mit einer Thunk-Klasse davor.

    Du kannst das Tool nicht auf statische Binaries anwerfen. Du musst Libs nehmen, die mit -fPIC kompiliert wurden. Du kannst ja einfach das ganze Programm als Lib zusammenfügen und dann testen.


Anmelden zum Antworten