Quelles techniques peuvent être utilisées pour accélérer les temps de compilation C ++?


249

Quelles techniques peuvent être utilisées pour accélérer les temps de compilation C ++?

Cette question a été soulevée dans certains commentaires sur le style de programmation de la question C ++ de Stack Overflow , et je suis intéressé d'entendre quelles idées il y a.

J'ai vu une question connexe, pourquoi la compilation C ++ prend-elle autant de temps? , mais cela ne fournit pas beaucoup de solutions.


1
Pourriez-vous nous donner un peu de contexte? Ou cherchez-vous des réponses très générales?
Pyrolistical

1
Très similaire à cette question: stackoverflow.com/questions/364240/…
Adam Rosenfield

Réponses générales. J'ai une très grande base de code écrite par beaucoup de gens. Des idées sur la façon d'attaquer ce serait bien. Et aussi, des suggestions pour garder les compilations rapides pour le code nouvellement écrit seraient intéressantes.
Scott Langham

Notez que souvent une partie pertinente du temps de construction n'est pas utilisée par le compilateur mais par les scripts de construction
thi gg

1
J'ai parcouru cette page et je n'ai vu aucune mention de mesures. J'ai écrit un petit script shell qui ajoute un horodatage à chaque ligne d'entrée qu'il reçoit, donc je peux simplement diriger l'invocation «make». Cela me permet de voir quelles cibles sont les plus chères, le temps total de compilation ou de liaison, etc. simplement en comparant les horodatages. Si vous essayez cette approche, n'oubliez pas que les horodatages seront inexacts pour les générations parallèles.
John P

Réponses:


257

Techniques langagières

Pimpl Idiom

Jetez un œil à l' idiome Pimpl ici et ici , également connu sous le nom de pointeur opaque ou de classes de descripteurs . Non seulement il accélère la compilation, mais il augmente également la sécurité des exceptions lorsqu'il est combiné avec une fonction d' échange sans lancement . L'idiome Pimpl vous permet de réduire les dépendances entre les en-têtes et réduit la quantité de recompilation à effectuer.

Déclarations à terme

Dans la mesure du possible, utilisez des déclarations à terme . Si le compilateur a seulement besoin de savoir qu'il SomeIdentifiers'agit d'une structure ou d'un pointeur ou autre, n'incluez pas la définition entière, forçant le compilateur à faire plus de travail qu'il n'en a besoin. Cela peut avoir un effet en cascade, ce qui rend cette façon plus lente que nécessaire.

Les flux d' E / S sont particulièrement connus pour ralentir les builds. Si vous en avez besoin dans un fichier d'en-tête, essayez #including <iosfwd>au lieu de <iostream>et #include l'en- <iostream>tête dans le fichier d'implémentation uniquement. L'en- <iosfwd>tête contient uniquement les déclarations avancées. Malheureusement, les autres en-têtes standard n'ont pas d'en-tête de déclaration respectif.

Préférez le passage par référence au passage par valeur dans les signatures de fonction. Cela éliminera le besoin d'inclure les définitions de type respectives dans le fichier d'en-tête et vous n'aurez qu'à déclarer le type en amont. Bien sûr, préférez les références const aux références non const pour éviter les bugs obscurs, mais c'est un problème pour une autre question.

Conditions de garde

Utilisez des conditions de garde pour empêcher les fichiers d'en-tête d'être inclus plusieurs fois dans une seule unité de traduction.

#pragma once
#ifndef filename_h
#define filename_h

// Header declarations / definitions

#endif

En utilisant à la fois le pragma et l'ifndef, vous obtenez la portabilité de la solution de macro simple, ainsi que l'optimisation de la vitesse de compilation que certains compilateurs peuvent faire en présence de la pragma oncedirective.

Réduisez l'interdépendance

Plus votre conception de code est modulaire et moins interdépendante en général, moins vous devrez tout recompiler. Vous pouvez également finir par réduire la quantité de travail que le compilateur doit faire sur n'importe quel bloc individuel en même temps, en raison du fait qu'il a moins à suivre.

Options du compilateur

En-têtes précompilés

Ils sont utilisés pour compiler une section commune des en-têtes inclus une fois pour de nombreuses unités de traduction. Le compilateur le compile une fois et enregistre son état interne. Cet état peut ensuite être chargé rapidement pour obtenir une longueur d'avance dans la compilation d'un autre fichier avec ce même ensemble d'en-têtes.

Veillez à n'inclure que des éléments rarement modifiés dans les en-têtes précompilés, ou vous pourriez finir par effectuer des reconstructions complètes plus souvent que nécessaire. C'est un bon endroit pour les en- têtes STL et les autres fichiers d'inclusion de bibliothèque.

ccache est un autre utilitaire qui tire parti des techniques de mise en cache pour accélérer les choses.

Utiliser le parallélisme

De nombreux compilateurs / IDE prennent en charge l'utilisation de plusieurs cœurs / CPU pour effectuer la compilation simultanément. Dans GNU Make (généralement utilisé avec GCC), utilisez l' -j [N]option. Dans Visual Studio, il y a une option sous les préférences pour lui permettre de construire plusieurs projets en parallèle. Vous pouvez également utiliser l' /MPoption pour le paralellisme au niveau du fichier, au lieu du seul paralellisme au niveau du projet.

Autres utilitaires parallèles:

Utiliser un niveau d'optimisation inférieur

Plus le compilateur essaie d'optimiser, plus il doit travailler dur.

Bibliothèques partagées

Déplacer votre code moins fréquemment modifié dans des bibliothèques peut réduire le temps de compilation. En utilisant des bibliothèques partagées ( .soou .dll), vous pouvez également réduire le temps de liaison.

Obtenez un ordinateur plus rapide

Plus de RAM, des disques durs plus rapides (y compris les SSD) et plus de CPU / cœurs feront tous une différence dans la vitesse de compilation.


11
Les en-têtes précompilés ne sont cependant pas parfaits. Un effet secondaire de leur utilisation est que vous obtenez plus de fichiers inclus que nécessaire (car chaque unité de compilation utilise le même en-tête précompilé), ce qui peut forcer les recompilations complètes plus souvent que nécessaire. Juste quelque chose à garder à l'esprit.
jalf

8
Dans les compilateurs modernes, #ifndef est aussi rapide que #pragma une fois (tant que la garde d'inclusion est en haut du fichier). Il n'y a donc aucun avantage à #pragma une fois en termes de vitesse de compilation
jalf

7
Même si vous n'avez que VS 2005, pas 2008, vous pouvez ajouter le commutateur / MP dans les options de compilation pour activer la construction parallèle au niveau .cpp.
macbirdie

6
Les disques SSD étaient prohibitifs lorsque cette réponse a été écrite, mais aujourd'hui, ils sont le meilleur choix lors de la compilation de C ++. Vous accédez à de nombreux petits fichiers lors de la compilation ;. Cela nécessite beaucoup d'IOPS, que les SSD fournissent.
MSalters

14
Préférez le passage par référence au passage par valeur dans les signatures de fonction. Cela permettra d' éliminer la nécessité de #inclure les définitions de type respectives dans le fichier d' en- tête Ceci est mauvais , vous n'avez pas besoin d'avoir le type complet de déclarer une fonction qui passe par la valeur, vous avez seulement besoin du type complet de mettre en œuvre ou utiliser cette fonction , mais dans la plupart des cas (à moins que vous ne renvoyiez que des appels), vous aurez quand même besoin de cette définition.
David Rodríguez - dribeas

43

Je travaille sur le projet STAPL qui est une bibliothèque C ++ fortement basée sur des modèles. De temps en temps, nous devons revoir toutes les techniques pour réduire le temps de compilation. Ici, j'ai résumé les techniques que nous utilisons. Certaines de ces techniques sont déjà répertoriées ci-dessus:

Trouver les sections les plus chronophages

Bien qu'il n'y ait pas de corrélation prouvée entre les longueurs de symboles et le temps de compilation, nous avons observé que des tailles moyennes de symboles plus petites peuvent améliorer le temps de compilation sur tous les compilateurs. Donc, votre premier objectif est de trouver les plus grands symboles dans votre code.

Méthode 1 - Trier les symboles en fonction de la taille

Vous pouvez utiliser la nmcommande pour répertorier les symboles en fonction de leur taille:

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

Dans cette commande, le --radix=dvous permet de voir les tailles en nombres décimaux (la valeur par défaut est hexadécimale). Maintenant, en regardant le plus grand symbole, identifiez si vous pouvez séparer la classe correspondante et essayez de la repenser en factorisant les parties non basées sur un modèle dans une classe de base, ou en divisant la classe en plusieurs classes.

Méthode 2 - Trier les symboles en fonction de la longueur

Vous pouvez exécuter la nmcommande standard et la diriger vers votre script préféré ( AWK , Python , etc.) pour trier les symboles en fonction de leur longueur . D'après notre expérience, cette méthode identifie les problèmes les plus importants pour rendre les candidats meilleurs que la méthode 1.

Méthode 3 - Utilisez Templight

" Templight est un outil basé sur Clang pour profiler la consommation de temps et de mémoire des instanciations de modèles et pour effectuer des sessions de débogage interactives afin de gagner en introspection dans le processus d'instanciation de modèles".

Vous pouvez installer Templight en vérifiant LLVM et Clang ( instructions ) et en y appliquant le correctif Templight. Le paramètre par défaut pour LLVM et Clang concerne le débogage et les assertions, et ceux-ci peuvent avoir un impact significatif sur votre temps de compilation. Il semble que Templight ait besoin des deux, vous devez donc utiliser les paramètres par défaut. Le processus d'installation de LLVM et Clang devrait prendre environ une heure.

Après avoir appliqué le correctif, vous pouvez utiliser templight++situé dans le dossier de construction que vous avez spécifié lors de l'installation pour compiler votre code.

Assurez-vous que cela se templight++trouve dans votre CHEMIN. Maintenant, pour compiler, ajoutez les commutateurs suivants à votre CXXFLAGSdans votre Makefile ou à vos options de ligne de commande:

CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system

Ou

templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system

Une fois la compilation terminée, vous aurez un .trace.memory.pbf et .trace.pbf générés dans le même dossier. Pour visualiser ces traces, vous pouvez utiliser les outils Templight qui peuvent les convertir en d'autres formats. Suivez ces instructions pour installer templight-convert. Nous utilisons généralement la sortie callgrind. Vous pouvez également utiliser la sortie GraphViz si votre projet est petit:

$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace

$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot

Le fichier callgrind généré peut être ouvert à l'aide de kcachegrind dans lequel vous pouvez tracer l'instanciation la plus consommatrice de temps / mémoire.

Réduction du nombre d'instanciations de modèles

Bien qu'il n'y ait pas de solution exacte pour réduire le nombre d'instanciations de modèles, il existe quelques directives qui peuvent vous aider:

Classes de refactorisation avec plusieurs arguments de modèle

Par exemple, si vous avez une classe,

template <typename T, typename U>
struct foo { };

et à la fois Tet Upeuvent avoir 10 options différentes, vous avez augmenté les instanciations de modèle possibles de cette classe à 100. Une façon de résoudre ce problème est d'abstraire la partie commune du code à une classe différente. L'autre méthode consiste à utiliser l'inversion d'héritage (inversion de la hiérarchie des classes), mais assurez-vous que vos objectifs de conception ne sont pas compromis avant d'utiliser cette technique.

Refactoriser le code non modélisé en unités de traduction individuelles

En utilisant cette technique, vous pouvez compiler une fois la section commune et la lier à vos autres UT (unités de traduction) ultérieurement.

Utiliser des instanciations de modèles externes (depuis C ++ 11)

Si vous connaissez toutes les instanciations possibles d'une classe, vous pouvez utiliser cette technique pour compiler tous les cas dans une unité de traduction différente.

Par exemple, dans:

enum class PossibleChoices = {Option1, Option2, Option3}

template <PossibleChoices pc>
struct foo { };

Nous savons que cette classe peut avoir trois instanciations possibles:

template class foo<PossibleChoices::Option1>;
template class foo<PossibleChoices::Option2>;
template class foo<PossibleChoices::Option3>;

Mettez ce qui précède dans une unité de traduction et utilisez le mot clé extern dans votre fichier d'en-tête, sous la définition de classe:

extern template class foo<PossibleChoices::Option1>;
extern template class foo<PossibleChoices::Option2>;
extern template class foo<PossibleChoices::Option3>;

Cette technique peut vous faire gagner du temps si vous compilez différents tests avec un ensemble commun d'instanciations.

REMARQUE: MPICH2 ignore l'instanciation explicite à ce stade et compile toujours les classes instanciées dans toutes les unités de compilation.

Utiliser des builds d'unité

L'idée derrière les builds d'unité est d'inclure tous les fichiers .cc que vous utilisez dans un seul fichier et de compiler ce fichier une seule fois. En utilisant cette méthode, vous pouvez éviter de réinstaurer des sections communes de différents fichiers et si votre projet comprend un grand nombre de fichiers communs, vous économiserez probablement également sur les accès au disque.

À titre d'exemple, supposons que vous avez trois fichiers foo1.cc, foo2.cc, foo3.ccet ils comprennent tous tuplede STL . Vous pouvez créer un foo-all.ccqui ressemble à:

#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"

Vous compilez ce fichier une seule fois et réduisez potentiellement les instanciations courantes entre les trois fichiers. Il est généralement difficile de prédire si l'amélioration peut être significative ou non. Mais un fait évident est que vous perdriez le parallélisme dans vos builds (vous ne pouvez plus compiler les trois fichiers en même temps).

De plus, si l'un de ces fichiers nécessite beaucoup de mémoire, vous risquez de manquer de mémoire avant la fin de la compilation. Sur certains compilateurs, tels que GCC , cela pourrait provoquer une erreur ICE (Internal Compiler Error) sur votre compilateur par manque de mémoire. N'utilisez donc pas cette technique à moins de connaître tous les avantages et les inconvénients.

En-têtes précompilés

Les en-têtes précompilés (PCH) peuvent vous faire gagner beaucoup de temps lors de la compilation en compilant vos fichiers d'en-tête dans une représentation intermédiaire reconnaissable par un compilateur. Pour générer des fichiers d'en-tête précompilés, il vous suffit de compiler votre fichier d'en-tête avec votre commande de compilation régulière. Par exemple, sur GCC:

$ g++ YOUR_HEADER.hpp

Cela générera un YOUR_HEADER.hpp.gch file( .gchest l'extension pour les fichiers PCH dans GCC) dans le même dossier. Cela signifie que si vous incluez YOUR_HEADER.hppdans un autre fichier, le compilateur utilisera votre YOUR_HEADER.hpp.gchau lieu de YOUR_HEADER.hppdans le même dossier auparavant.

Il y a deux problèmes avec cette technique:

  1. Vous devez vous assurer que les fichiers d'en-tête précompilés sont stables et ne vont pas changer ( vous pouvez toujours changer votre makefile )
  2. Vous ne pouvez inclure qu'un seul PCH par unité de compilation (sur la plupart des compilateurs). Cela signifie que si vous avez plusieurs fichiers d'en-tête à précompiler, vous devez les inclure dans un fichier (par exemple, all-my-headers.hpp). Mais cela signifie que vous devez inclure le nouveau fichier à tous les endroits. Heureusement, GCC a une solution à ce problème. Utilisez -includeet donnez-lui le nouveau fichier d'en-tête. Vous pouvez séparer par virgule différents fichiers à l'aide de cette technique.

Par exemple:

g++ foo.cc -include all-my-headers.hpp

Utiliser des espaces de noms sans nom ou anonymes

Les espaces de noms sans nom (ou espaces de noms anonymes) peuvent réduire considérablement les tailles binaires générées. Les espaces de noms sans nom utilisent une liaison interne, ce qui signifie que les symboles générés dans ces espaces de noms ne seront pas visibles par les autres UT (unités de traduction ou de compilation). Les compilateurs génèrent généralement des noms uniques pour les espaces de noms sans nom. Cela signifie que si vous avez un fichier foo.hpp:

namespace {

template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;

Et il se trouve que vous incluez ce fichier dans deux UT (deux fichiers .cc et les compilez séparément). Les deux instances de modèle foo ne seront pas identiques. Cela viole la règle de définition unique (ODR). Pour la même raison, l'utilisation d'espaces de noms sans nom est déconseillée dans les fichiers d'en-tête. N'hésitez pas à les utiliser dans vos .ccfichiers pour éviter que des symboles n'apparaissent dans vos fichiers binaires. Dans certains cas, la modification de tous les détails internes d'un .ccfichier a montré une réduction de 10% des tailles binaires générées.

Modification des options de visibilité

Dans les nouveaux compilateurs, vous pouvez sélectionner vos symboles pour qu'ils soient visibles ou invisibles dans les objets partagés dynamiques (DSO). Idéalement, la modification de la visibilité peut améliorer les performances du compilateur, les optimisations de temps de liaison (LTO) et les tailles binaires générées. Si vous regardez les fichiers d'en-tête STL dans GCC, vous pouvez voir qu'il est largement utilisé. Pour activer les choix de visibilité, vous devez modifier votre code par fonction, par classe, par variable et, plus important encore, par compilateur.

Avec l'aide de la visibilité, vous pouvez masquer les symboles que vous considérez comme privés des objets partagés générés. Sur GCC, vous pouvez contrôler la visibilité des symboles en passant par défaut ou masqué à l' -visibilityoption de votre compilateur. Ceci est en quelque sorte similaire à l'espace de noms sans nom, mais d'une manière plus élaborée et intrusive.

Si vous souhaitez spécifier les visibilités par cas, vous devez ajouter les attributs suivants à vos fonctions, variables et classes:

__attribute__((visibility("default"))) void  foo1() { }
__attribute__((visibility("hidden")))  void  foo2() { }
__attribute__((visibility("hidden")))  class foo3   { };
void foo4() { }

La visibilité par défaut dans GCC est default (public), ce qui signifie que si vous compilez ce qui précède en tant que -sharedméthode library ( ) partagée , foo2et que la classe foo3ne sera pas visible dans les autres TU ( foo1et foo4sera visible). Si vous compilez avec -visibility=hiddenalors seulement foo1sera visible. Même foo4serait caché.

Vous pouvez en savoir plus sur la visibilité sur le wiki de GCC .


33

Je recommanderais ces articles de "Games from Within, Indie Game Design And Programming":

Certes, ils sont assez vieux - vous devrez tout tester à nouveau avec les dernières versions (ou les versions disponibles), pour obtenir des résultats réalistes. Quoi qu'il en soit, c'est une bonne source d'idées.


17

Une technique qui a très bien fonctionné pour moi dans le passé: ne compilez pas plusieurs fichiers source C ++ indépendamment, mais générez plutôt un fichier C ++ qui inclut tous les autres fichiers, comme ceci:

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

Bien sûr, cela signifie que vous devez recompiler tout le code source inclus au cas où l'une des sources changerait, de sorte que l'arbre de dépendance s'aggrave. Cependant, la compilation de plusieurs fichiers source en une seule unité de traduction est plus rapide (au moins dans mes expériences avec MSVC et GCC) et génère des binaires plus petits. Je soupçonne également que le compilateur a plus de potentiel d'optimisation (car il peut voir plus de code à la fois).

Cette technique casse dans divers cas; par exemple, le compilateur se renflouera au cas où deux fichiers source ou plus déclarent une fonction globale avec le même nom. Je n'ai pas trouvé cette technique décrite dans aucune des autres réponses, c'est pourquoi je la mentionne ici.

Pour ce que ça vaut, le projet KDE a utilisé cette même technique depuis 1999 pour construire des binaires optimisés (éventuellement pour une version). Le basculement vers le script de configuration de build a été appelé --enable-final. Par intérêt archéologique, j'ai déterré l'affichage qui annonçait cette fonctionnalité: http://lists.kde.org/?l=kde-devel&m=92722836009368&w=2


2
Je ne suis pas sûr que ce soit vraiment la même chose, mais je suppose que l'activation de "Whole program optimization" dans VC ++ ( msdn.microsoft.com/en-us/library/0zza0de8%28VS.71%29.aspx ) devrait avoir le même effet sur les performances d'exécution que ce que vous proposez. Compiler le temps, cependant, peut certainement être meilleur dans votre approche!
Philipp

1
@Frerich: Vous décrivez les versions d'Unity mentionnées dans la réponse d'OJ. Je les ai également vus appelés builds en vrac et builds principaux.
idbrii

Alors, comment un UB se compare-t-il au WPO / LTCG?
paulm

Ceci n'est potentiellement utile que pour les compilations ponctuelles, pas pendant le développement où vous passez de l'édition, la construction et les tests. Dans le monde moderne, quatre cœurs sont la norme, peut-être quelques années plus tard, le nombre de cœurs est beaucoup plus élevé. Si le compilateur et l'éditeur de liens ne sont pas en mesure d'utiliser plusieurs threads, la liste des fichiers pourrait peut-être être divisée en <core-count> + Nsous-listes qui sont compilées en parallèle où se Ntrouve un entier approprié (en fonction de la mémoire système et de la façon dont la machine est utilisée autrement).
FooF

15

Il y a un livre entier sur ce sujet, qui s'intitule Large-Scale C ++ Software Design (écrit par John Lakos).

Le livre précède les modèles, donc au contenu de ce livre, ajoutez «l'utilisation de modèles peut également ralentir le compilateur».


Le livre est souvent mentionné dans ce genre de sujets, mais pour moi, il était rare en informations. Il indique essentiellement d'utiliser autant que possible les déclarations avancées et de découpler les dépendances. Cela indique un peu plus que l'utilisation de l'idiome pimpl a des inconvénients d'exécution.
gast128

@ gast128 Je pense que son but est d'utiliser des idiomes de codage qui permettent une recompilation incrémentielle, c'est-à-dire que si vous changez un peu de source quelque part, vous n'aurez pas à tout recompiler.
ChrisW

15

Je vais simplement créer un lien vers mon autre réponse: comment réduisez-vous le temps de compilation et le temps de liaison pour les projets Visual C ++ (C ++ natif)? . Un autre point que je veux ajouter, mais qui pose souvent des problèmes, est d'utiliser des en-têtes précompilés. Mais s'il vous plaît, utilisez-les uniquement pour les pièces qui ne changent presque jamais (comme les en-têtes de la boîte à outils GUI). Sinon, ils vous coûteront plus de temps qu'ils ne vous feront économiser au final.

Une autre option est, lorsque vous travaillez avec GNU make, d'activer l' -j<N>option:

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

Je l'ai généralement 3depuis que j'ai un dual core ici. Il exécutera ensuite des compilateurs en parallèle pour différentes unités de traduction, à condition qu'il n'y ait pas de dépendances entre eux. La liaison ne peut pas être effectuée en parallèle, car il n'existe qu'un seul processus de liaison liant tous les fichiers objets.

Mais l'éditeur de liens lui-même peut être enfilé, et c'est ce que fait l' éditeur de liens ELF . Il s'agit d'un code C ++ threadé optimisé qui est censé lier les fichiers objets ELF une amplitude plus rapide que l'ancien (et qui était en fait inclus dans binutils ).GNU gold ld


OK oui. Désolé, cette question ne s'est pas posée lorsque j'ai cherché.
Scott Langham

vous ne deviez pas être désolé. c'était pour Visual C ++. votre question semble s'adresser à tout compilateur. donc ça va :)
Johannes Schaub - litb

12

Voilà quelque:

  • Utilisez tous les cœurs de processeur en démarrant une tâche de compilation multiple ( make -j2c'est un bon exemple).
  • Désactiver ou diminuer les optimisations (par exemple, GCC est beaucoup plus rapide avec -O1que -O2ou -O3).
  • Utilisez des en- têtes précompilés .

12
Pour info, je trouve qu'il est généralement plus rapide de démarrer plus de processus que de cœurs. Par exemple, sur un système quad core, j'utilise généralement -j8, pas -j4. La raison en est que lorsqu'un processus est bloqué sur les E / S, l'autre peut être en cours de compilation.
M. Fooz

@MrFooz: J'ai testé cela il y a quelques années en compilant le noyau Linux (à partir du stockage RAM) sur un i7-2700k (4 cœurs, 8 threads, j'ai défini un multiplicateur constant). J'oublie le meilleur résultat exact, mais -j12à environ -j18étaient beaucoup plus vite que -j8, comme vous le suggérez. Je me demande combien de cœurs vous pouvez avoir avant que la bande passante mémoire ne devienne le facteur limitant ...
Mark K Cowan

@MarkKCowan cela dépend de beaucoup de facteurs. Différents ordinateurs ont des bandes passantes mémoire extrêmement différentes. Avec les processeurs haut de gamme de nos jours, il faut plusieurs cœurs pour saturer le bus mémoire. Il y a aussi l'équilibre entre les E / S et le CPU. Certains codes sont très faciles à compiler, d'autres peuvent être lents (par exemple avec de nombreux modèles). Ma règle de base actuelle est -javec 2x le nombre de cœurs réels.
M. Fooz

11

Une fois que vous avez appliqué toutes les astuces de code ci-dessus (déclarations avancées, réduire l'inclusion d'en-tête au minimum dans les en-têtes publics, pousser la plupart des détails dans le fichier d'implémentation avec Pimpl ...) et que rien d'autre ne peut être obtenu en termes de langue, pensez à votre système de construction . Si vous utilisez Linux, pensez à utiliser distcc (compilateur distribué) et ccache (compilateur de cache).

Le premier, distcc, exécute l'étape du préprocesseur localement, puis envoie la sortie au premier compilateur disponible du réseau. Il nécessite les mêmes versions de compilateur et de bibliothèque dans tous les nœuds configurés du réseau.

Ce dernier, ccache, est un cache de compilateur. Il exécute à nouveau le préprocesseur, puis vérifie auprès d'une base de données interne (conservée dans un répertoire local) si ce fichier de préprocesseur a déjà été compilé avec les mêmes paramètres de compilateur. Si c'est le cas, il affiche simplement le binaire et la sortie de la première exécution du compilateur.

Les deux peuvent être utilisés en même temps, de sorte que si ccache n'a pas de copie locale, il peut l'envoyer via le net à un autre nœud avec distcc, ou bien il peut simplement injecter la solution sans traitement supplémentaire.


2
Je ne pense pas que distcc nécessite les mêmes versions de bibliothèque sur tous les nœuds configurés. distcc ne fait que la compilation à distance, pas la liaison. Il envoie également le code prétraité sur le fil, de sorte que les en-têtes disponibles sur le système distant n'ont pas d'importance.
Frerich Raabe

9

Quand je suis sorti de l'université, le premier vrai code C ++ digne de production que j'ai vu avait ces directives arcanes #ifndef ... #endif entre elles où les en-têtes étaient définis. J'ai demandé au gars qui écrivait le code de ces choses primordiales d'une manière très naïve et a été initié au monde de la programmation à grande échelle.

Pour en revenir au fait, l'utilisation de directives pour éviter les définitions d'en-tête en double a été la première chose que j'ai apprise en matière de réduction des temps de compilation.


1
Vieux mais de qualité. parfois l'évidence est oubliée.
alcor

1
'inclure les gardes'
gast128

8

Plus de RAM.

Quelqu'un a parlé des lecteurs RAM dans une autre réponse. Je l'ai fait avec un 80286 et un Turbo C ++ (montre l'âge) et les résultats ont été phénoménaux. Tout comme la perte de données lorsque la machine s'est écrasée.


sous DOS, vous ne pouvez pas avoir beaucoup de mémoire
phuclv

6

Utilisez des déclarations avancées lorsque vous le pouvez. Si une déclaration de classe utilise uniquement un pointeur ou une référence à un type, vous pouvez simplement le déclarer et inclure l'en-tête du type dans le fichier d'implémentation.

Par exemple:

// 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
}

Moins d'inclure signifie beaucoup moins de travail pour le préprocesseur si vous le faites suffisamment.


Cela n'a-t-il pas d'importance uniquement lorsque le même en-tête est inclus dans plusieurs unités de traduction? S'il n'y a qu'une seule unité de traduction (comme c'est souvent le cas lorsque des modèles sont utilisés), cela ne semble pas avoir d'impact.
AlwaysLearning

1
S'il n'y a qu'une seule unité de traduction, pourquoi se donner la peine de la mettre dans un en-tête? Ne serait-il pas plus logique de simplement mettre le contenu dans le fichier source? N'est-ce pas l'intérêt des en-têtes qu'il est susceptible d'être inclus dans plusieurs fichiers source?
Evan Teran


5

Utilisation

#pragma once

en haut des fichiers d'en-tête, donc s'ils sont inclus plus d'une fois dans une unité de traduction, le texte de l'en-tête ne sera inclus et analysé qu'une seule fois.


2
Bien que largement pris en charge, #pragma est une fois non standard. Voir en.wikipedia.org/wiki/Pragma_once
ChrisInEdmonton

7
Et ces jours-ci, les gardes incluent régulièrement ont le même effet. Tant qu'ils sont en haut du fichier, le compilateur est tout à fait capable de les traiter comme #pragma fois
jalf


4
  • Mettez à niveau votre ordinateur

    1. Obtenez un quad core (ou un système dual-quad)
    2. Obtenez BEAUCOUP de RAM.
    3. Utilisez un lecteur RAM pour réduire considérablement les retards d'E / S des fichiers. (Il existe des sociétés qui fabriquent des disques RAM IDE et SATA qui agissent comme des disques durs).
  • Ensuite, vous avez toutes vos autres suggestions typiques

    1. Utilisez des en-têtes précompilés si disponibles.
    2. Réduisez la quantité de couplage entre les parties de votre projet. La modification d'un fichier d'en-tête ne devrait généralement pas nécessiter de recompiler l'intégralité de votre projet.

4

J'ai eu une idée sur l' utilisation d'un lecteur RAM . Il s'est avéré que pour mes projets, cela ne fait pas beaucoup de différence après tout. Mais alors ils sont encore assez petits. Essayez! J'aimerais savoir à quel point cela a aidé.


Huh. Pourquoi quelqu'un a-t-il voté contre? Je vais l'essayer demain.
Scott Langham

1
Je m'attends à ce que le downvote soit parce qu'il ne fait jamais une grande différence. Si vous avez suffisamment de RAM inutilisée, le système d'exploitation l'utilisera de manière intelligente comme cache disque de toute façon.
MSalters

1
@MSalters - et combien serait "suffisant"? Je sais que c'est la théorie, mais pour une raison quelconque, l'utilisation d'un RAMdrive donne en fait un coup de pouce significatif. Allez comprendre ...
Vilx

1
assez pour compiler votre projet et toujours mettre en cache les fichiers d'entrée et temporaires. Évidemment, le côté en Go dépendra directement de la taille de votre projet. Il convient de noter que sur les anciens systèmes d'exploitation (WinXP en particulier), les caches de fichiers étaient assez paresseux, laissant la RAM inutilisée.
MSalters

Certes, le lecteur RAM est plus rapide si les fichiers sont déjà dans le RAM plutôt que de faire tout un tas d'E / S lentes en premier, alors ils sont dans le RAM? (augmenter-répéter pour les fichiers qui ont changé - les réécrire sur le disque, etc.).
paulm

3

La liaison dynamique (.so) peut être beaucoup plus rapide que la liaison statique (.a). Surtout lorsque vous avez un lecteur réseau lent. C'est parce que vous avez tout le code dans le fichier .a qui doit être traité et écrit. De plus, un fichier exécutable beaucoup plus volumineux doit être écrit sur le disque.


la liaison dynamique empêche de nombreux types d'optimisations de temps de liaison, de sorte que la sortie peut être plus lente dans de nombreux cas
phuclv

3

Pas sur le temps de compilation, mais sur le temps de construction:

  • Utilisez ccache si vous devez reconstruire les mêmes fichiers lorsque vous travaillez sur vos fichiers de build

  • Utilisation ninja-build au lieu de make. Je compile actuellement un projet avec ~ 100 fichiers sources et tout est mis en cache par ccache. faire des besoins 5 minutes, ninja moins de 1.

Vous pouvez générer vos fichiers ninja à partir de cmake avec -GNinja.


3

Où passez-vous votre temps? Êtes-vous lié au processeur? Mémoire liée? Disque lié? Pouvez-vous utiliser plus de cœurs? Plus de RAM? Avez-vous besoin d'un RAID? Voulez-vous simplement améliorer l'efficacité de votre système actuel?

Sous gcc / g ++, avez-vous regardé ccache ? Cela peut être utile si vous en faites make clean; makebeaucoup.


2

Disques durs plus rapides.

Les compilateurs écrivent de nombreux (et peut-être d'énormes) fichiers sur le disque. Travaillez avec SSD au lieu du disque dur typique et les temps de compilation sont beaucoup plus courts.


2

Sous Linux (et peut-être d'autres * NIX), vous pouvez vraiment accélérer la compilation en NE DÉMARRANT PAS à la sortie et en changeant pour un autre TTY.

Voici l'expérience: printf ralentit mon programme


2

Les partages réseaux ralentiront considérablement votre build, car la latence de recherche est élevée. Pour quelque chose comme Boost, cela a fait une énorme différence pour moi, même si notre lecteur de partage réseau est assez rapide. Le temps de compilation d'un programme Boost jouet est passé d'environ 1 minute à 1 seconde lorsque je suis passé d'un partage réseau à un SSD local.


2

Si vous avez un processeur multicœur, Visual Studio (2005 et versions ultérieures) ainsi que GCC prennent en charge les compilations multiprocesseurs. C'est quelque chose à activer si vous avez le matériel, c'est sûr.


2
@Fellman, Voir certaines des autres réponses - utilisez l'option -j #.
strager

1

Bien que ce ne soit pas une "technique", je n'ai pas pu comprendre comment les projets Win32 avec de nombreux fichiers source compilés plus rapidement que mon projet vide "Hello World". Ainsi, j'espère que cela aide quelqu'un comme moi.

Dans Visual Studio, une option pour augmenter les temps de compilation est la liaison incrémentielle ( / INCREMENTAL ). Il est incompatible avec la génération de code au moment de la liaison ( / LTCG ), alors n'oubliez pas de désactiver la liaison incrémentielle lors des générations de versions.


1
désactiver la génération de code au moment de la liaison n'est pas une bonne suggestion car cela désactive de nombreuses optimisations. Vous devez activer /INCREMENTALen mode débogage uniquement
phuclv

1

À partir de Visual Studio 2017, vous avez la possibilité d'avoir des mesures de compilation sur ce qui prend du temps.

Ajoutez ces paramètres à C / C ++ -> Ligne de commande (Options supplémentaires) dans la fenêtre des propriétés du projet: /Bt+ /d2cgsummary /d1reportTime

Vous pouvez avoir plus d'informations dans cet article .


0

L'utilisation d'une liaison dynamique au lieu d'une liaison statique rend votre compilateur plus rapide que vous pouvez ressentir.

Si vous utilisez t Cmake, activez la propriété:

set(BUILD_SHARED_LIBS ON)

Build Release, l'utilisation de liens statiques peut être plus optimisée.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.