Ich arbeite am STAPL-Projekt, einer C ++ - Bibliothek mit starken Vorlagen. Hin und wieder müssen wir alle Techniken überdenken, um die Kompilierungszeit zu verkürzen. Hier habe ich die Techniken zusammengefasst, die wir verwenden. Einige dieser Techniken sind bereits oben aufgeführt:
Finden der zeitaufwändigsten Abschnitte
Obwohl es keine nachgewiesene Korrelation zwischen den Symbollängen und der Kompilierungszeit gibt, haben wir beobachtet, dass kleinere durchschnittliche Symbolgrößen die Kompilierungszeit auf allen Compilern verbessern können. Ihr erstes Ziel ist es also, die größten Symbole in Ihrem Code zu finden.
Methode 1 - Sortieren Sie Symbole nach Größe
Mit dem nm
Befehl können Sie die Symbole anhand ihrer Größe auflisten:
nm --print-size --size-sort --radix=d YOUR_BINARY
In diesem Befehl --radix=d
können Sie die Größen in Dezimalzahlen anzeigen (Standard ist hexadezimal). Sehen Sie sich nun das größte Symbol an, um festzustellen, ob Sie die entsprechende Klasse aufteilen können, und versuchen Sie, sie neu zu gestalten, indem Sie die nicht mit Vorlagen versehenen Teile in einer Basisklasse berücksichtigen oder die Klasse in mehrere Klassen aufteilen.
Methode 2 - Sortieren Sie Symbole nach Länge
Sie können die reguläre ausführen nm
Befehl und an Ihr Lieblingsskript ( AWK , Python usw.) weiterleiten , um die Symbole nach ihrer Länge zu sortieren . Basierend auf unserer Erfahrung identifiziert diese Methode die größten Probleme, Kandidaten besser zu machen als Methode 1.
Methode 3 - Verwenden Sie Templight
" Templight ist ein Clang- basiertes Tool zum Profilieren des Zeit- und Speicherverbrauchs von Vorlageninstanziierungen und zum Durchführen interaktiver Debugging-Sitzungen, um einen Einblick in den Vorlageninstanziierungsprozess zu erhalten."
Sie können Templight installieren, indem Sie auschecken LLVM und Clang ( Anweisungen ) auschecken und den Templight-Patch darauf anwenden. Die Standardeinstellung für LLVM und Clang ist Debugging und Assertions. Diese können sich erheblich auf Ihre Kompilierungszeit auswirken. Templight benötigt anscheinend beides, daher müssen Sie die Standardeinstellungen verwenden. Die Installation von LLVM und Clang sollte ungefähr eine Stunde dauern.
Nach dem Anwenden des Patches können templight++
Sie den Code verwenden, der sich in dem Build-Ordner befindet, den Sie bei der Installation angegeben haben.
Stellen Sie sicher, dass dies templight++
in Ihrem PFAD ist. Fügen Sie nun zum Kompilieren die folgenden Schalter zu Ihrem hinzuCXXFLAGS
Ihrem Makefile oder zu Ihren Befehlszeilenoptionen hinzu:
CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
Oder
templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
Nach Abschluss der Kompilierung werden im selben Ordner eine .trace.memory.pbf und eine .trace.pbf generiert. Um diese Traces zu visualisieren, können Sie die Templight-Tools verwenden, mit denen diese in andere Formate konvertiert werden können. Befolgen Sie diese Anweisungen , um templight-convert zu installieren. Wir verwenden normalerweise die Callgrind-Ausgabe. Sie können die GraphViz-Ausgabe auch verwenden, wenn Ihr Projekt klein ist:
$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace
$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot
Die generierte Callgrind-Datei kann mit geöffnet werden kcachegrind in dem Sie die zeit- und speicherintensivste Instanziierung verfolgen können.
Reduzieren der Anzahl der Vorlageninstanziierungen
Obwohl es keine genaue Lösung gibt, um die Anzahl der Vorlageninstanziierungen zu verringern, gibt es einige Richtlinien, die helfen können:
Refactor-Klassen mit mehr als einem Vorlagenargument
Wenn Sie beispielsweise eine Klasse haben,
template <typename T, typename U>
struct foo { };
und beide T
und U
können 10 verschiedene Optionen haben. Sie haben die möglichen Vorlageninstanziierungen dieser Klasse auf 100 erhöht. Eine Möglichkeit, dies zu beheben, besteht darin, den gemeinsamen Teil des Codes in eine andere Klasse zu abstrahieren. Die andere Methode ist die Verwendung der Vererbungsinversion (Umkehrung der Klassenhierarchie). Stellen Sie jedoch sicher, dass Ihre Entwurfsziele nicht beeinträchtigt werden, bevor Sie diese Technik verwenden.
Refactor-Code ohne Vorlagen für einzelne Übersetzungseinheiten
Mit dieser Technik können Sie den allgemeinen Abschnitt einmal kompilieren und später mit Ihren anderen TUs (Übersetzungseinheiten) verknüpfen.
Externe Vorlageninstanziierungen verwenden (seit C ++ 11)
Wenn Sie alle möglichen Instanziierungen einer Klasse kennen, können Sie mit dieser Technik alle Fälle in einer anderen Übersetzungseinheit kompilieren.
Zum Beispiel in:
enum class PossibleChoices = {Option1, Option2, Option3}
template <PossibleChoices pc>
struct foo { };
Wir wissen, dass diese Klasse drei mögliche Instanziierungen haben kann:
template class foo<PossibleChoices::Option1>;
template class foo<PossibleChoices::Option2>;
template class foo<PossibleChoices::Option3>;
Fügen Sie das Obige in eine Übersetzungseinheit ein und verwenden Sie das Schlüsselwort extern in Ihrer Header-Datei unterhalb der Klassendefinition:
extern template class foo<PossibleChoices::Option1>;
extern template class foo<PossibleChoices::Option2>;
extern template class foo<PossibleChoices::Option3>;
Diese Technik kann Ihnen Zeit sparen, wenn Sie verschiedene Tests mit einem gemeinsamen Satz von Instanziierungen kompilieren.
HINWEIS: MPICH2 ignoriert die explizite Instanziierung an dieser Stelle und kompiliert immer die instanziierten Klassen in allen Kompilierungseinheiten.
Verwenden Sie Unity Builds
Die ganze Idee hinter Unity Builds besteht darin, alle von Ihnen verwendeten .cc-Dateien in eine Datei aufzunehmen und diese Datei nur einmal zu kompilieren. Mit dieser Methode können Sie vermeiden, gemeinsame Abschnitte verschiedener Dateien wiederherzustellen, und wenn Ihr Projekt viele gemeinsame Dateien enthält, würden Sie wahrscheinlich auch beim Zugriff auf die Festplatte sparen.
Als Beispiel nehmen wir an , Sie drei Dateien haben foo1.cc
, foo2.cc
, foo3.cc
und sie alle sind tuple
von STL . Sie können eine erstellen foo-all.cc
, die wie folgt aussieht:
#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"
Sie kompilieren diese Datei nur einmal und reduzieren möglicherweise die allgemeinen Instanziierungen zwischen den drei Dateien. Es ist schwer vorherzusagen, ob die Verbesserung signifikant sein kann oder nicht. Eine offensichtliche Tatsache ist jedoch, dass Sie die Parallelität in Ihren Builds verlieren würden (Sie können die drei Dateien nicht mehr gleichzeitig kompilieren).
Wenn eine dieser Dateien viel Speicherplatz beansprucht, geht Ihnen möglicherweise der Speicherplatz aus, bevor die Kompilierung abgeschlossen ist. Auf einigen Compilern, wie z. B. GCC , kann dies zu ICE (Internal Compiler Error) Ihres Compilers führen, da nicht genügend Speicher vorhanden ist. Verwenden Sie diese Technik also nur, wenn Sie alle Vor- und Nachteile kennen.
Vorkompilierte Header
Vorkompilierte Header (PCHs) können Ihnen beim Kompilieren viel Zeit sparen, indem Sie Ihre Header-Dateien zu einer Zwischendarstellung kompilieren, die von einem Compiler erkannt wird. Um vorkompilierte Header-Dateien zu generieren, müssen Sie Ihre Header-Datei nur mit Ihrem regulären Kompilierungsbefehl kompilieren. Zum Beispiel auf GCC:
$ g++ YOUR_HEADER.hpp
Dadurch wird im selben Ordner ein YOUR_HEADER.hpp.gch file
( .gch
ist die Erweiterung für PCH-Dateien in GCC) generiert . Dies bedeutet, dass YOUR_HEADER.hpp
der Compiler , wenn Sie in eine andere Datei aufnehmen, Ihre YOUR_HEADER.hpp.gch
anstelle von YOUR_HEADER.hpp
im selben Ordner verwendet.
Bei dieser Technik gibt es zwei Probleme:
- Sie müssen sicherstellen, dass die vorkompilierten Header-Dateien stabil sind und sich nicht ändern ( Sie können Ihr Makefile jederzeit ändern ).
- Sie können nur einen PCH pro Kompilierungseinheit einschließen (bei den meisten Compilern). Dies bedeutet, dass wenn Sie mehr als eine Header-Datei vorkompilieren müssen, Sie diese in eine Datei aufnehmen müssen (z
all-my-headers.hpp
. B. ). Das bedeutet aber, dass Sie die neue Datei an allen Stellen einfügen müssen. Glücklicherweise hat GCC eine Lösung für dieses Problem. Verwenden Sie -include
und geben Sie ihm die neue Header-Datei. Mit dieser Technik können Sie verschiedene Dateien durch Kommas trennen.
Beispielsweise:
g++ foo.cc -include all-my-headers.hpp
Verwenden Sie unbenannte oder anonyme Namespaces
Unbenannte Namespaces (auch anonyme Namespaces genannt) können die generierten Binärgrößen erheblich reduzieren. Unbenannte Namespaces verwenden interne Verknüpfungen. Dies bedeutet, dass die in diesen Namespaces generierten Symbole für andere TU (Übersetzungs- oder Kompilierungseinheiten) nicht sichtbar sind. Compiler generieren normalerweise eindeutige Namen für unbenannte Namespaces. Dies bedeutet, wenn Sie eine Datei foo.hpp haben:
namespace {
template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;
Und Sie fügen diese Datei zufällig in zwei TUs ein (zwei .cc-Dateien und kompilieren sie separat). Die beiden foo-Vorlageninstanzen sind nicht identisch. Dies verstößt gegen die One Definition Rule (ODR). Aus dem gleichen Grund wird davon abgeraten, unbenannte Namespaces in den Header-Dateien zu verwenden. Sie können sie auch in Ihren .cc
Dateien verwenden, um zu vermeiden, dass Symbole in Ihren Binärdateien angezeigt werden. In einigen Fällen führte das Ändern aller internen Details für eine .cc
Datei zu einer Reduzierung der generierten Binärgrößen um 10%.
Sichtbarkeitsoptionen ändern
In neueren Compilern können Sie Ihre Symbole so auswählen, dass sie in den Dynamic Shared Objects (DSOs) entweder sichtbar oder unsichtbar sind. Im Idealfall kann das Ändern der Sichtbarkeit die Compilerleistung verbessern, Verbindungszeitoptimierungen (LTOs) und generierte Binärgrößen generieren. Wenn Sie sich die STL-Header-Dateien in GCC ansehen, sehen Sie, dass sie weit verbreitet sind. Um die Auswahl der Sichtbarkeit zu ermöglichen, müssen Sie Ihren Code pro Funktion, pro Klasse, pro Variable und vor allem pro Compiler ändern.
Mithilfe der Sichtbarkeit können Sie die Symbole, die Sie als privat betrachten, vor den generierten freigegebenen Objekten ausblenden. In GCC können Sie die Sichtbarkeit von Symbolen steuern, indem Sie standardmäßig oder ausgeblendet an die -visibility
Option Ihres Compilers übergeben. Dies ähnelt in gewisser Weise dem unbenannten Namespace, ist jedoch aufwändiger und aufdringlicher.
Wenn Sie die Sichtbarkeiten pro Fall angeben möchten, müssen Sie Ihren Funktionen, Variablen und Klassen die folgenden Attribute hinzufügen:
__attribute__((visibility("default"))) void foo1() { }
__attribute__((visibility("hidden"))) void foo2() { }
__attribute__((visibility("hidden"))) class foo3 { };
void foo4() { }
Die Standardsichtbarkeit in GCC ist Standard (öffentlich). Wenn Sie die oben genannte -shared
Methode als gemeinsam genutzte Bibliothek ( ) kompilieren, wird die foo2
Klasse foo3
in anderen TUs nicht sichtbar ( foo1
und foo4
ist auch sichtbar). Wenn Sie mit kompilieren, -visibility=hidden
ist nur foo1
dann sichtbar. Sogar foo4
wäre versteckt.
Weitere Informationen zur Sichtbarkeit finden Sie im GCC-Wiki .