Welche Techniken können verwendet werden, um die C ++ - Kompilierungszeiten zu beschleunigen?


249

Welche Techniken können verwendet werden, um die C ++ - Kompilierungszeiten zu beschleunigen?

Diese Frage tauchte in einigen Kommentaren zum Programmierstil der Stapelüberlauffrage C ++ auf , und ich bin gespannt, welche Ideen es gibt.

Ich habe eine verwandte Frage gesehen: Warum dauert die C ++ - Kompilierung so lange? , aber das bietet nicht viele Lösungen.


1
Könnten Sie uns einen Kontext geben? Oder suchen Sie sehr allgemeine Antworten?
Pyrolistical

1
Sehr ähnlich zu dieser Frage: stackoverflow.com/questions/364240/…
Adam Rosenfield

Allgemeine Antworten. Ich habe eine wirklich große Codebasis, die von vielen Leuten geschrieben wurde. Ideen, wie man das angreift, wären gut. Interessant wären auch Vorschläge, wie Kompilierungen für neu geschriebenen Code schnell durchgeführt werden können.
Scott Langham

Beachten Sie, dass oft ein relevantes Teil der Build - Zeit nicht durch den Compiler , sondern durch den Build - Skripte verwendet wird
thi gg

1
Ich habe diese Seite überflogen und keine Erwähnungen von Messungen gesehen. Ich habe ein kleines Shell-Skript geschrieben, das jeder empfangenen Eingabezeile einen Zeitstempel hinzufügt, damit ich den Aufruf 'make' einfach weiterleiten kann. Auf diese Weise kann ich durch Vergleichen der Zeitstempel feststellen, welche Ziele am teuersten sind, wie lange die Kompilierung oder Verknüpfung insgesamt dauert. Wenn Sie diesen Ansatz ausprobieren, denken Sie daran, dass die Zeitstempel für parallele Builds ungenau sind.
John P

Antworten:


257

Sprachtechniken

Pimpl Idiom

Schauen Sie sich hier und hier die Pimpl-Sprache an , die auch als undurchsichtige Zeiger- oder Handle-Klassen bezeichnet wird. Dies beschleunigt nicht nur die Kompilierung, sondern erhöht auch die Ausnahmesicherheit in Kombination mit einem nicht werfenden Tausch Funktion. Mit der Pimpl-Sprache können Sie die Abhängigkeiten zwischen Headern reduzieren und den Umfang der Neukompilierung reduzieren, die durchgeführt werden muss.

Vorwärtserklärungen

Verwenden Sie nach Möglichkeit Vorwärtserklärungen . Wenn der Compiler das nur wissen mussSomeIdentifier es sich um eine Struktur oder einen Zeiger oder was auch immer handelt, schließen Sie nicht die gesamte Definition ein, sodass der Compiler mehr Arbeit leisten muss als nötig. Dies kann einen Kaskadeneffekt haben und diesen Weg langsamer machen, als sie sein müssen.

Die E / A- Streams sind besonders dafür bekannt, Builds zu verlangsamen. Wenn Sie sie in einer Header-Datei benötigen, versuchen Sie, #including <iosfwd>anstelle von <iostream>und #include den <iostream>Header nur in die Implementierungsdatei aufzunehmen. Der <iosfwd>Header enthält nur Vorwärtsdeklarationen. Leider haben die anderen Standardheader keinen entsprechenden Deklarationsheader.

Bei Funktionssignaturen ist die Referenzübergabe der Wertübergabe vorzuziehen. Dadurch entfällt die Notwendigkeit, # die entsprechenden Typdefinitionen in die Header-Datei aufzunehmen, und Sie müssen den Typ nur vorwärts deklarieren. Bevorzugen Sie natürlich const-Verweise gegenüber nicht-const-Verweisen, um obskure Fehler zu vermeiden, aber dies ist ein Problem für eine andere Frage.

Wachbedingungen

Verwenden Sie Schutzbedingungen, um zu verhindern, dass Header-Dateien mehrmals in einer einzelnen Übersetzungseinheit enthalten sind.

#pragma once
#ifndef filename_h
#define filename_h

// Header declarations / definitions

#endif

Wenn Sie sowohl das Pragma als auch das ifndef verwenden, erhalten Sie die Portabilität der einfachen Makrolösung sowie die Optimierung der Kompilierungsgeschwindigkeit, die einige Compiler in Gegenwart von ausführen können pragma once Direktive durchführen können.

Reduzieren Sie die gegenseitige Abhängigkeit

Je modularer und weniger voneinander abhängig Ihr Code-Design im Allgemeinen ist, desto seltener müssen Sie alles neu kompilieren. Sie können auch den Arbeitsaufwand reduzieren, den der Compiler gleichzeitig für einen einzelnen Block leisten muss, da er weniger zu verfolgen hat.

Compiler-Optionen

Vorkompilierte Header

Diese werden verwendet, um einen gemeinsamen Abschnitt der enthaltenen Überschriften einmal für viele Übersetzungseinheiten zu kompilieren. Der Compiler kompiliert es einmal und speichert seinen internen Status. Dieser Status kann dann schnell geladen werden, um einen Vorsprung beim Kompilieren einer anderen Datei mit denselben Headern zu erhalten.

Achten Sie darauf, dass Sie nur selten geänderte Inhalte in die vorkompilierten Header aufnehmen, da Sie sonst häufiger als nötig vollständige Neuerstellungen durchführen können. Dies ist ein guter Ort für STL- Header und andere Bibliotheks-Include-Dateien.

ccache ist ein weiteres Dienstprogramm, das Caching-Techniken nutzt, um die Dinge zu beschleunigen.

Verwenden Sie Parallelität

Viele Compiler / IDEs unterstützen die Verwendung mehrerer Kerne / CPUs, um gleichzeitig zu kompilieren. Verwenden Sie in GNU Make (normalerweise mit GCC verwendet) die -j [N]Option. In Visual Studio gibt es unter den Einstellungen eine Option, mit der mehrere Projekte parallel erstellt werden können. Sie können die Option auch verwenden/MP für Paralellismus auf Dateiebene verwenden, anstatt nur Paralellismus auf Projektebene.

Andere parallele Dienstprogramme:

Verwenden Sie eine niedrigere Optimierungsstufe

Je mehr der Compiler zu optimieren versucht, desto schwieriger muss er arbeiten.

Gemeinsame Bibliotheken

Das Verschieben Ihres weniger häufig geänderten Codes in Bibliotheken kann die Kompilierungszeit verkürzen. Durch die Verwendung gemeinsam genutzter Bibliotheken ( .sooder .dll) können Sie auch die Verknüpfungszeit verkürzen.

Holen Sie sich einen schnelleren Computer

Mehr RAM, schnellere Festplatten (einschließlich SSDs) und mehr CPUs / Kerne wirken sich auf die Kompilierungsgeschwindigkeit aus.


11
Vorkompilierte Header sind jedoch nicht perfekt. Ein Nebeneffekt bei der Verwendung ist, dass mehr Dateien als erforderlich enthalten sind (da jede Kompilierungseinheit denselben vorkompilierten Header verwendet), wodurch möglicherweise eine vollständige Neukompilierung häufiger als erforderlich erzwungen wird. Nur etwas zu beachten.
Jalf

8
In modernen Compilern ist #ifndef einmal genauso schnell wie #pragma (solange sich der Include-Guard oben in der Datei befindet). #Pragma hat also keinen Vorteil in Bezug auf die Kompilierungsgeschwindigkeit
jalf

7
Selbst wenn Sie nur VS 2005 und nicht 2008 haben, können Sie in den Kompilierungsoptionen einen / MP-Schalter hinzufügen, um die parallele Erstellung auf CPP-Ebene zu ermöglichen.
Macbirdie

6
SSDs waren unerschwinglich teuer, als diese Antwort geschrieben wurde, aber heute sind sie die beste Wahl beim Kompilieren von C ++. Sie greifen beim Kompilieren auf viele kleine Dateien zu. Das erfordert eine Menge IOPS, die SSDs liefern.
MSalters

14
Bei Funktionssignaturen ist die Referenzübergabe der Wertübergabe vorzuziehen. Dadurch entfällt die Notwendigkeit, die entsprechenden Typdefinitionen in die Header-Datei aufzunehmen. Dies ist falsch . Sie müssen nicht den vollständigen Typ haben, um eine Funktion zu deklarieren, die als Wert übergeben wird. Sie benötigen nur den vollständigen Typ, um diese Funktion zu implementieren oder zu verwenden In den meisten Fällen (es sei denn, Sie leiten nur Anrufe weiter) benötigen Sie diese Definition trotzdem.
David Rodríguez - Dribeas

43

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 nmBefehl können Sie die Symbole anhand ihrer Größe auflisten:

nm --print-size --size-sort --radix=d YOUR_BINARY

In diesem Befehl --radix=dkö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 Tund Ukö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.ccund sie alle sind tuplevon 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( .gchist die Erweiterung für PCH-Dateien in GCC) generiert . Dies bedeutet, dass YOUR_HEADER.hppder Compiler , wenn Sie in eine andere Datei aufnehmen, Ihre YOUR_HEADER.hpp.gchanstelle von YOUR_HEADER.hppim selben Ordner verwendet.

Bei dieser Technik gibt es zwei Probleme:

  1. Sie müssen sicherstellen, dass die vorkompilierten Header-Dateien stabil sind und sich nicht ändern ( Sie können Ihr Makefile jederzeit ändern ).
  2. 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 -includeund 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 .ccDateien 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 .ccDatei 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 -visibilityOption 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 -sharedMethode als gemeinsam genutzte Bibliothek ( ) kompilieren, wird die foo2Klasse foo3in anderen TUs nicht sichtbar ( foo1und foo4ist auch sichtbar). Wenn Sie mit kompilieren, -visibility=hiddenist nur foo1dann sichtbar. Sogar foo4wäre versteckt.

Weitere Informationen zur Sichtbarkeit finden Sie im GCC-Wiki .


33

Ich würde diese Artikel aus "Games from Within, Indie Game Design und Programmierung" empfehlen:

Zugegeben, sie sind ziemlich alt - Sie müssen alles mit den neuesten Versionen (oder Versionen, die Ihnen zur Verfügung stehen) erneut testen, um realistische Ergebnisse zu erzielen. In jedem Fall ist es eine gute Quelle für Ideen.


17

Eine Technik, die in der Vergangenheit für mich sehr gut funktioniert hat: Kompilieren Sie nicht mehrere C ++ - Quelldateien unabhängig voneinander, sondern generieren Sie eine C ++ - Datei, die alle anderen Dateien enthält, wie folgt:

// myproject_all.cpp
// Automatically generated file - don't edit this by hand!
#include "main.cpp"
#include "mainwindow.cpp"
#include "filterdialog.cpp"
#include "database.cpp"

Dies bedeutet natürlich, dass Sie den gesamten enthaltenen Quellcode neu kompilieren müssen, falls sich eine der Quellen ändert, damit sich der Abhängigkeitsbaum verschlechtert. Das Kompilieren mehrerer Quelldateien als eine Übersetzungseinheit ist jedoch schneller (zumindest in meinen Experimenten mit MSVC und GCC) und generiert kleinere Binärdateien. Ich vermute auch, dass der Compiler mehr Potenzial für Optimierungen hat (da er mehr Code auf einmal sehen kann).

Diese Technik bricht in verschiedenen Fällen; Beispielsweise wird der Compiler aussteigen, wenn zwei oder mehr Quelldateien eine globale Funktion mit demselben Namen deklarieren. Ich konnte diese Technik in keiner der anderen Antworten finden, deshalb erwähne ich sie hier.

Für das, was es wert ist, verwendete das KDE-Projekt seit 1999 genau dieselbe Technik, um optimierte Binärdateien zu erstellen (möglicherweise für eine Veröffentlichung). Der Wechsel zum Build-Konfigurationsskript wurde aufgerufen --enable-final. Aus archäologischem Interesse habe ich den Beitrag ausgegraben, in dem diese Funktion angekündigt wurde: http://lists.kde.org/?l=kde-devel&m=92722836009368&w=2


2
Ich bin mir nicht sicher, ob es wirklich dasselbe ist, aber ich denke, das Aktivieren der "Optimierung des gesamten Programms" in VC ++ ( msdn.microsoft.com/en-us/library/0zza0de8%28VS.71%29.aspx ) sollte das haben Dieselbe Auswirkung auf die Laufzeitleistung als von Ihnen vorgeschlagen. Die Kompilierungszeit kann jedoch definitiv besser sein!
Philipp

1
@Frerich: Sie beschreiben Unity-Builds, die in der Antwort von OJ erwähnt werden. Ich habe auch gesehen, dass sie Bulk-Builds und Master-Builds genannt werden.
idbrii

Wie vergleicht sich ein UB mit WPO / LTCG?
Paulm

Dies ist möglicherweise nur für einmalige Kompilierungen nützlich, nicht während der Entwicklung, bei der Sie zwischen Bearbeiten, Erstellen und Testen wechseln. In der modernen Welt sind vier Kerne die Norm, vielleicht ein paar Jahre später ist die Anzahl der Kerne deutlich höher. Wenn der Compiler und der Linker nicht in der Lage sind, mehrere Threads zu verwenden, kann die Liste der Dateien möglicherweise in <core-count> + NUnterlisten aufgeteilt werden, die parallel kompiliert werden, wobei Neine geeignete Ganzzahl vorliegt (abhängig vom Systemspeicher und der Art und Weise, wie der Computer anderweitig verwendet wird).
FooF

15

Zu diesem Thema gibt es ein ganzes Buch mit dem Titel Large-Scale C ++ Software Design (geschrieben von John Lakos).

Das Buch datiert Vorlagen vor, sodass zum Inhalt dieses Buches hinzugefügt wird, dass "auch die Verwendung von Vorlagen den Compiler langsamer machen kann".


Das Buch wird oft in solchen Themen erwähnt, aber für mich gab es nur wenige Informationen. Grundsätzlich heißt es, Forward-Deklarationen so weit wie möglich zu verwenden und Abhängigkeiten zu entkoppeln. Das ist ein bisschen das Offensichtliche, außerdem hat die Verwendung der Pimpl-Sprache Laufzeitnachteile.
Gast128

@ gast128 Ich denke, es geht darum, Codierungssprachen zu verwenden, die eine inkrementelle Neukompilierung ermöglichen, dh wenn Sie irgendwo ein wenig die Quelle ändern, müssen Sie nicht alles neu kompilieren.
ChrisW

15

Ich werde nur auf meine andere Antwort verweisen: Wie reduzieren SIE die Kompilierungszeit und die Verknüpfungszeit für Visual C ++ - Projekte (natives C ++)? . Ein weiterer Punkt, den ich hinzufügen möchte, der jedoch häufig Probleme verursacht, ist die Verwendung vorkompilierter Header. Verwenden Sie sie jedoch nur für Teile, die sich kaum ändern (z. B. GUI-Toolkit-Header). Andernfalls kosten sie Sie mehr Zeit als Sie am Ende sparen.

Eine andere Option ist, wenn Sie mit GNU make arbeiten, die -j<N>Option zu aktivieren:

  -j [N], --jobs[=N]          Allow N jobs at once; infinite jobs with no arg.

Normalerweise habe ich es, 3da ich hier einen Dual Core habe. Anschließend werden Compiler für verschiedene Übersetzungseinheiten parallel ausgeführt, sofern keine Abhängigkeiten zwischen ihnen bestehen. Die Verknüpfung kann nicht parallel erfolgen, da nur ein Verknüpfungsprozess alle Objektdateien miteinander verknüpft.

Der Linker selbst kann jedoch mit einem Thread versehen werden, und genau das tut der ELF- Linker. Es handelt sich um optimierten C ++ - Thread-Code, der ELF-Objektdateien eine Größenordnung schneller verknüpfen soll als der alte (und tatsächlich in binutils enthalten war ).GNU gold ld


OK ja. Entschuldigung, diese Frage ist bei der Suche nicht aufgetaucht.
Scott Langham

Es musste dir nicht leid tun. das war für Visual C ++. Ihre Frage scheint für jeden Compiler zu sein. also ist das in Ordnung :)
Johannes Schaub - litb

12

Hier sind einige:

  • Verwenden Sie alle Prozessorkerne, indem Sie einen Job mit mehreren Kompilierungen starten ( make -j2ein gutes Beispiel).
  • Schalten Sie oder Optimierungen senken (zB GCC ist viel schneller mit -O1als -O2oder -O3).
  • Verwenden Sie vorkompilierte Header .

12
Zu Ihrer Information, ich finde es normalerweise schneller, mehr Prozesse als Kerne zu starten. Zum Beispiel verwende ich auf einem Quad-Core-System normalerweise -j8, nicht -j4. Der Grund dafür ist, dass wenn ein Prozess auf E / A blockiert ist, der andere kompiliert werden kann.
Herr Fooz

@ MrFooz: Ich habe dies vor einigen Jahren getestet, indem ich den Linux-Kernel (aus dem RAM-Speicher) auf einem i7-2700k kompiliert habe (4 Kerne, 8 Threads, ich habe einen konstanten Multiplikator festgelegt). Ich vergesse das genau beste Ergebnis, aber -j12um herum -j18waren erheblich schneller als -j8, wie Sie vorschlagen. Ich frage mich, wie viele Kerne Sie haben können, bevor die Speicherbandbreite zum begrenzenden Faktor wird ...
Mark K Cowan

@ MarkKCowan es hängt von vielen Faktoren ab. Unterschiedliche Computer haben sehr unterschiedliche Speicherbandbreiten. Bei High-End-Prozessoren sind heutzutage mehrere Kerne erforderlich, um den Speicherbus zu sättigen. Außerdem besteht das Gleichgewicht zwischen E / A und CPU. Einige Codes sind sehr einfach zu kompilieren, andere können langsam sein (z. B. mit vielen Vorlagen). Meine derzeitige Faustregel lautet: -j2x die Anzahl der tatsächlichen Kerne.
Herr Fooz

11

Wenn Sie alle oben genannten Code-Tricks angewendet haben (Vorwärtsdeklarationen, Reduzierung der Header-Aufnahme in öffentlichen Headern auf das Minimum, Verschieben der meisten Details in die Implementierungsdatei mit Pimpl ...) und sprachlich nichts anderes erreicht werden kann, sollten Sie Ihr Build-System in Betracht ziehen . Wenn Sie Linux verwenden, sollten Sie distcc (verteilter Compiler) und ccache (Cache-Compiler) verwenden.

Der erste, distcc, führt den Präprozessorschritt lokal aus und sendet dann die Ausgabe an den ersten verfügbaren Compiler im Netzwerk. Es erfordert die gleichen Compiler- und Bibliotheksversionen in allen konfigurierten Knoten im Netzwerk.

Letzterer, ccache, ist ein Compiler-Cache. Der Präprozessor wird erneut ausgeführt und anschließend mit einer internen Datenbank (in einem lokalen Verzeichnis gespeichert) überprüft, ob diese Präprozessordatei bereits mit denselben Compilerparametern kompiliert wurde. Wenn dies der Fall ist, wird nur die Binärdatei angezeigt und vom ersten Lauf des Compilers ausgegeben.

Beide können gleichzeitig verwendet werden, so dass ccache, wenn es keine lokale Kopie hat, diese über das Netz mit distcc an einen anderen Knoten senden kann, oder die Lösung einfach ohne weitere Verarbeitung injizieren kann.


2
Ich denke nicht, dass distcc auf allen konfigurierten Knoten die gleichen Bibliotheksversionen erfordert . distcc führt die Kompilierung nur remote durch, nicht die Verknüpfung. Es sendet auch den vorverarbeiteten Code über das Kabel, sodass die auf dem Remote-System verfügbaren Header keine Rolle spielen.
Frerich Raabe

9

Als ich das College verließ, hatte der erste echte produktionswürdige C ++ - Code, den ich sah, diese arkanen # ifndef ... # endif-Direktiven dazwischen, in denen die Header definiert waren. Ich fragte den Mann, der den Code auf sehr naive Weise über diese übergreifenden Dinge schrieb, und wurde in die Welt der groß angelegten Programmierung eingeführt.

Zurück zum Punkt: Die Verwendung von Anweisungen zur Vermeidung doppelter Headerdefinitionen war das erste, was ich gelernt habe, wenn es darum ging, die Kompilierungszeiten zu verkürzen.


1
alt aber gut. manchmal wird das Offensichtliche vergessen.
Alcor

1
'Wachen einschließen'
gast128

8

Mehr RAM.

Jemand sprach in einer anderen Antwort über RAM-Laufwerke. Ich habe dies mit einem 80286 und Turbo C ++ (zeigt Alter) gemacht und die Ergebnisse waren phänomenal. Wie war der Datenverlust beim Absturz der Maschine.


unter DOS kann man allerdings nicht viel Speicher haben
phuclv

6

Verwenden Sie Vorwärtsdeklarationen, wo Sie können. Wenn eine Klassendeklaration nur einen Zeiger oder Verweis auf einen Typ verwendet, können Sie ihn einfach weiterleiten und den Header für den Typ in die Implementierungsdatei aufnehmen.

Beispielsweise:

// T.h
class Class2; // Forward declaration

class T {
public:
    void doSomething(Class2 &c2);
private:
    Class2 *m_Class2Ptr;
};

// T.cpp
#include "Class2.h"
void Class2::doSomething(Class2 &c2) {
    // Whatever you want here
}

Weniger Includes bedeuten viel weniger Arbeit für den Präprozessor, wenn Sie es genug tun.


Ist das nicht nur wichtig, wenn derselbe Header in mehreren Übersetzungseinheiten enthalten ist? Wenn es nur eine Übersetzungseinheit gibt (wie dies häufig bei der Verwendung von Vorlagen der Fall ist), scheint dies keine Auswirkungen zu haben.
AlwaysLearning

1
Wenn es nur eine Übersetzungseinheit gibt, warum sollte man sie dann in eine Kopfzeile einfügen? Wäre es nicht sinnvoller, den Inhalt einfach in die Quelldatei einzufügen? Ist es nicht der springende Punkt bei Headern, dass sie wahrscheinlich in mehr als einer Quelldatei enthalten sind?
Evan Teran


5

Verwenden

#pragma once

Wenn sie also mehr als einmal in einer Übersetzungseinheit enthalten sind, wird der Text der Kopfzeile nur einmal eingefügt und analysiert.


2
Obwohl weit verbreitet, ist #pragma einmal nicht Standard. Siehe en.wikipedia.org/wiki/Pragma_once
ChrisInEdmonton

7
Und heutzutage haben regelmäßige Wachen den gleichen Effekt. Solange sie sich oben in der Datei befinden, kann der Compiler sie einmalig als #pragma behandeln
am


4
  • Aktualisieren Sie Ihren Computer

    1. Holen Sie sich einen Quad-Core (oder ein Dual-Quad-System)
    2. Holen Sie sich viel RAM.
    3. Verwenden Sie ein RAM-Laufwerk, um die Verzögerungen bei Datei-E / A drastisch zu reduzieren. (Es gibt Unternehmen, die IDE- und SATA-RAM-Laufwerke herstellen, die sich wie Festplatten verhalten.)
  • Dann haben Sie alle Ihre anderen typischen Vorschläge

    1. Verwenden Sie vorkompilierte Header, falls verfügbar.
    2. Reduzieren Sie die Kopplung zwischen Teilen Ihres Projekts. Das Ändern einer Header-Datei sollte normalerweise nicht das Neukompilieren Ihres gesamten Projekts erfordern.

4

Ich hatte eine Idee zur Verwendung eines RAM-Laufwerks . Es stellte sich heraus, dass es für meine Projekte doch keinen großen Unterschied macht. Aber dann sind sie noch ziemlich klein. Versuch es! Es würde mich interessieren, wie sehr es geholfen hat.


Huh. Warum hat jemand dies abgelehnt? Ich werde es morgen versuchen.
Scott Langham

1
Ich gehe davon aus, dass die Ablehnung darin besteht, dass sie niemals einen großen Unterschied macht. Wenn Sie über ausreichend unbenutzten RAM verfügen, wird dieser vom Betriebssystem ohnehin intelligent als Festplatten-Cache verwendet.
MSalters

1
@ MSalters - und wie viel wäre "ausreichend"? Ich weiß, daß die Theorie ist, aber aus irgendeinem Grund eine RAMdrive mit nicht tatsächlich einen deutlichen Schub geben. Go figure ...
Vilx

1
genug, um Ihr Projekt zu kompilieren und trotzdem die Eingabe- und temporären Dateien zwischenzuspeichern. Offensichtlich hängt die Seite in GB direkt von Ihrer Projektgröße ab. Es sollte beachtet werden, dass Dateicaches unter älteren Betriebssystemen (insbesondere WinXP) ziemlich faul waren und RAM nicht verwendet wurden.
MSalters

Sicherlich ist das RAM-Laufwerk schneller, wenn sich die Dateien bereits im RAM befinden, anstatt zuerst eine ganze Reihe langsamer E / A-Vorgänge auszuführen. Dann befinden sie sich im RAM? (Anstieg-Wiederholung für Dateien, die sich geändert haben - schreiben Sie sie zurück auf die Festplatte usw.).
Paulm

3

Dynamische Verknüpfungen (.so) können viel schneller sein als statische Verknüpfungen (.a). Besonders wenn Sie ein langsames Netzlaufwerk haben. Dies liegt daran, dass Sie den gesamten Code in der .a-Datei haben, der verarbeitet und ausgeschrieben werden muss. Außerdem muss eine viel größere ausführbare Datei auf die Festplatte geschrieben werden.


Dynamische Verknüpfungen verhindern viele Arten von Optimierungen der Verknüpfungszeit, sodass die Ausgabe in vielen Fällen langsamer sein kann
phuclv

3

Nicht über die Kompilierungszeit, sondern über die Erstellungszeit:

  • Verwenden Sie ccache, wenn Sie dieselben Dateien neu erstellen müssen, wenn Sie an Ihren Builddateien arbeiten

  • Verwenden Sie Ninja-Build anstelle von make. Ich kompiliere gerade ein Projekt mit ~ 100 Quelldateien und alles wird von ccache zwischengespeichert. machen braucht 5 Minuten, Ninja weniger als 1.

Sie können Ihre Ninja-Dateien aus cmake mit generieren -GNinja.


3

Wo verbringst du deine Zeit? Sind Sie CPU-gebunden? Speicher gebunden? Festplattengebunden? Können Sie mehr Kerne verwenden? Mehr RAM? Benötigen Sie RAID? Möchten Sie einfach die Effizienz Ihres aktuellen Systems verbessern?

Haben Sie sich unter gcc / g ++ den Ccache angesehen ? Es kann hilfreich sein, wenn Sie make clean; makeviel tun .


2

Schnellere Festplatten.

Compiler schreiben viele (und möglicherweise riesige) Dateien auf die Festplatte. Arbeiten Sie mit SSD anstelle einer typischen Festplatte, und die Kompilierungszeiten sind viel kürzer.


2

Unter Linux (und möglicherweise einigen anderen * NIXes) können Sie die Kompilierung wirklich beschleunigen, indem Sie NICHT auf die Ausgabe starren und zu einem anderen TTY wechseln .

Hier ist das Experiment: printf verlangsamt mein Programm


2

Netzwerkfreigaben verlangsamen Ihren Build drastisch, da die Suchlatenz hoch ist. Für so etwas wie Boost hat es einen großen Unterschied gemacht, obwohl unser Netzwerkfreigabe-Laufwerk ziemlich schnell ist. Die Zeit zum Kompilieren eines Toy Boost-Programms stieg von etwa 1 Minute auf 1 Sekunde, als ich von einer Netzwerkfreigabe zu einer lokalen SSD wechselte.


2

Wenn Sie einen Multicore-Prozessor haben, unterstützen sowohl Visual Studio (2005 und höher) als auch GCC Multiprozessor-Kompilierungen. Es ist etwas zu aktivieren, wenn Sie die Hardware haben, sicher.


2
@Fellman, sehen Sie einige der anderen Antworten - verwenden Sie die Option -j #.
Strager

1

Obwohl dies keine "Technik" ist, konnte ich nicht herausfinden, wie Win32-Projekte mit vielen Quelldateien schneller kompiliert wurden als mein leeres Projekt "Hello World". Daher hoffe ich, dass dies jemandem hilft, wie es mir getan hat.

In Visual Studio ist eine Option zum Erhöhen der Kompilierungszeiten die inkrementelle Verknüpfung ( / INCREMENTAL ). Es ist nicht kompatibel mit Link-Time Code Generation ( / LTCG ). Denken Sie also daran, die inkrementelle Verknüpfung zu deaktivieren, wenn Sie Release-Builds durchführen.


1
Das Deaktivieren der Codegenerierung für die Verbindungszeit ist kein guter Vorschlag, da dadurch viele Optimierungen deaktiviert werden. Sie müssen /INCREMENTALnur im Debug-Modus
aktivieren

1

Ab Visual Studio 2017 können Sie einige Compiler-Metriken darüber erstellen, was Zeit kostet.

Fügen Sie diese Parameter zu C / C ++ -> Befehlszeile (Zusätzliche Optionen) im Projekteigenschaftenfenster hinzu: /Bt+ /d2cgsummary /d1reportTime

Weitere Informationen finden Sie in diesem Beitrag .


0

Wenn Sie anstelle einer statischen Verknüpfung eine dynamische Verknüpfung verwenden, wird der Compiler schneller und spürbarer.

Wenn Sie t Cmake verwenden, aktivieren Sie die Eigenschaft:

set(BUILD_SHARED_LIBS ON)

Build Release mit statischer Verknüpfung kann optimiert werden.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.