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 nm
commande pour répertorier les symboles en fonction de leur taille:
nm --print-size --size-sort --radix=d YOUR_BINARY
Dans cette commande, le --radix=d
vous 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 nm
commande 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 CXXFLAGS
dans 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 T
et U
peuvent 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.cc
et ils comprennent tous tuple
de STL . Vous pouvez créer un foo-all.cc
qui 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
( .gch
est l'extension pour les fichiers PCH dans GCC) dans le même dossier. Cela signifie que si vous incluez YOUR_HEADER.hpp
dans un autre fichier, le compilateur utilisera votre YOUR_HEADER.hpp.gch
au lieu de YOUR_HEADER.hpp
dans le même dossier auparavant.
Il y a deux problèmes avec cette technique:
- 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 )
- 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 -include
et 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 .cc
fichiers pour éviter que des symboles n'apparaissent dans vos fichiers binaires. Dans certains cas, la modification de tous les détails internes d'un .cc
fichier 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' -visibility
option 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 -shared
méthode library ( ) partagée , foo2
et que la classe foo3
ne sera pas visible dans les autres TU ( foo1
et foo4
sera visible). Si vous compilez avec -visibility=hidden
alors seulement foo1
sera visible. Même foo4
serait caché.
Vous pouvez en savoir plus sur la visibilité sur le wiki de GCC .