L'instanciation explicite permet de réduire les temps de compilation et la taille des objets
Ce sont les gains majeurs qu'il peut apporter. Ils proviennent des deux effets suivants décrits en détail dans les sections ci-dessous:
- supprimer les définitions des en-têtes pour empêcher les outils de génération de reconstruire les inclusions
- redéfinition d'objet
Supprimer les définitions des en-têtes
L'instanciation explicite vous permet de laisser des définitions dans le fichier .cpp.
Lorsque la définition est sur l'en-tête et que vous la modifiez, un système de construction intelligent recompilerait tous les inclusions, ce qui pourrait être des dizaines de fichiers, rendant la compilation insupportablement lente.
Mettre des définitions dans des fichiers .cpp présente l'inconvénient que les bibliothèques externes ne peuvent pas réutiliser le modèle avec leurs propres nouvelles classes, mais «Supprimer les définitions des en-têtes inclus mais aussi exposer les modèles à une API externe» ci-dessous montre une solution de contournement.
Voir des exemples concrets ci-dessous.
Gains de redéfinition d'objet: comprendre le problème
Si vous définissez simplement complètement un modèle sur un fichier d'en-tête, chaque unité de compilation qui inclut cet en-tête finit par compiler sa propre copie implicite du modèle pour chaque utilisation d'argument de modèle différente.
Cela signifie beaucoup d'utilisation inutile du disque et de temps de compilation.
Voici un exemple concret, dans lequel les deux main.cpp
et notmain.cpp
implicitement définissent en MyTemplate<int>
raison de son utilisation dans ces fichiers.
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
int main() {
std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
int notmain() { return MyTemplate<int>().f(1); }
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
template<class T>
struct MyTemplate {
T f(T t) { return t + 1; }
};
#endif
notmain.hpp
#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP
int notmain();
#endif
GitHub en amont .
Compilez et affichez les symboles avec nm
:
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o notmain.o notmain.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++ -Wall -Wextra -std=c++11 -pedantic-errors -o main.out notmain.o main.o
echo notmain.o
nm -C -S notmain.o | grep MyTemplate
echo main.o
nm -C -S main.o | grep MyTemplate
Production:
notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
De man nm
, nous voyons que cela W
signifie un symbole faible, que GCC a choisi car il s'agit d'une fonction de modèle. Le symbole faible signifie que le code généré implicitement pour a MyTemplate<int>
été compilé sur les deux fichiers.
La raison pour laquelle il n'explose pas au moment de la liaison avec plusieurs définitions est que l'éditeur de liens accepte plusieurs définitions faibles, et en choisit simplement une à mettre dans l'exécutable final.
Les nombres dans la sortie signifient:
0000000000000000
: adresse dans la section. Ce zéro est dû au fait que les modèles sont automatiquement placés dans leur propre section
0000000000000017
: taille du code généré pour eux
Nous pouvons le voir un peu plus clairement avec:
objdump -S main.o | c++filt
qui se termine par:
Disassembly of section .text._ZN10MyTemplateIiE1fEi:
0000000000000000 <MyTemplate<int>::f(int)>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 89 7d f8 mov %rdi,-0x8(%rbp)
c: 89 75 f4 mov %esi,-0xc(%rbp)
f: 8b 45 f4 mov -0xc(%rbp),%eax
12: 83 c0 01 add $0x1,%eax
15: 5d pop %rbp
16: c3 retq
et _ZN10MyTemplateIiE1fEi
est le nom mutilé deMyTemplate<int>::f(int)>
dont a c++filt
décidé de ne pas démêler.
Nous voyons donc qu'une section distincte est générée pour chaque instanciation de méthode unique, et que chacune d'elles prend bien sûr de l'espace dans les fichiers objets.
Solutions au problème de redéfinition des objets
Ce problème peut être évité en utilisant une instanciation explicite et soit:
garder la définition sur hpp et ajouter extern template
hpp pour les types qui vont être explicitement instanciés.
Comme expliqué à: utilisation d'un modèle externe (C ++ 11) extern template
empêche un modèle complètement défini d'être instancié par des unités de compilation, à l'exception de notre instanciation explicite. De cette façon, seule notre instanciation explicite sera définie dans les objets finaux:
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
template<class T>
struct MyTemplate {
T f(T t) { return t + 1; }
};
extern template class MyTemplate<int>;
#endif
mytemplate.cpp
#include "mytemplate.hpp"
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
int main() {
std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
int notmain() { return MyTemplate<int>().f(1); }
Inconvénient:
- si vous êtes une bibliothèque d'en-tête uniquement, vous forcez les projets externes à effectuer leur propre instanciation explicite. Si vous n'êtes pas une bibliothèque avec en-tête uniquement, cette solution est probablement la meilleure.
- si le type de modèle est défini dans votre propre projet et non comme un type intégré
int
, il semble que vous soyez obligé d'ajouter l'inclusion pour celui-ci sur l'en-tête, une déclaration avant ne suffit pas: modèle externe & types incomplets Cela augmente les dépendances d'en-tête un peu.
déplacer la définition sur le fichier cpp, ne laisser que la déclaration sur hpp, c'est-à-dire modifier l'exemple d'origine pour qu'il soit:
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
template<class T>
struct MyTemplate {
T f(T t);
};
#endif
mytemplate.cpp
#include "mytemplate.hpp"
template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }
template class MyTemplate<int>;
Inconvénient: les projets externes ne peuvent pas utiliser votre modèle avec leurs propres types. Vous êtes également obligé d'instancier explicitement tous les types. Mais c'est peut-être un avantage puisque les programmeurs n'oublieront pas.
garder la définition sur hpp et ajouter extern template
sur chaque inclus:
mytemplate.cpp
#include "mytemplate.hpp"
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
extern template class MyTemplate<int>;
int main() {
std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
extern template class MyTemplate<int>;
int notmain() { return MyTemplate<int>().f(1); }
Inconvénient: tous les inclus doivent ajouter le extern
à leurs fichiers CPP, ce que les programmeurs oublieront probablement de faire.
Avec l'une de ces solutions, nm
contient désormais:
notmain.o
U MyTemplate<int>::f(int)
main.o
U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)
nous voyons donc avoir mytemplate.o
une compilation de MyTemplate<int>
comme souhaité, tandis que notmain.o
et main.o
ne pas parce que U
signifie indéfini.
Supprimez les définitions des en-têtes inclus, mais exposez également les modèles d'une API externe dans une bibliothèque d'en-tête uniquement
Si votre bibliothèque n'est pas uniquement en-tête, le extern template
méthode fonctionnera, car l'utilisation de projets sera simplement liée à votre fichier objet, qui contiendra l'objet de l'instanciation de modèle explicite.
Cependant, pour les bibliothèques d'en-tête uniquement, si vous souhaitez les deux:
- accélérer la compilation de votre projet
- exposer les en-têtes en tant qu'API de bibliothèque externe pour que d'autres puissent l'utiliser
alors vous pouvez essayer l'une des solutions suivantes:
-
mytemplate.hpp
: définition du modèle
mytemplate_interface.hpp
: modèle de déclaration correspondant uniquement aux définitions de mytemplate_interface.hpp
, pas de définitions
mytemplate.cpp
: inclure mytemplate.hpp
et créer des instancitations explicites
main.cpp
et partout ailleurs dans la base de code: inclure mytemplate_interface.hpp
, pasmytemplate.hpp
-
mytemplate.hpp
: définition du modèle
mytemplate_implementation.hpp
: inclut mytemplate.hpp
et ajoute extern
à chaque classe qui sera instanciée
mytemplate.cpp
: inclure mytemplate.hpp
et créer des instancitations explicites
main.cpp
et partout ailleurs dans la base de code: inclure mytemplate_implementation.hpp
, pasmytemplate.hpp
Ou encore mieux peut-être pour plusieurs en-têtes: créez un dossier intf
/ impl
dans votre includes/
dossier et utilisez mytemplate.hpp
toujours comme nom.
L' mytemplate_interface.hpp
approche ressemble à ceci:
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
#include "mytemplate_interface.hpp"
template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }
#endif
mytemplate_interface.hpp
#ifndef MYTEMPLATE_INTERFACE_HPP
#define MYTEMPLATE_INTERFACE_HPP
template<class T>
struct MyTemplate {
T f(T t);
};
#endif
mytemplate.cpp
#include "mytemplate.hpp"
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate_interface.hpp"
int main() {
std::cout << MyTemplate<int>().f(1) << std::endl;
}
Compilez et exécutez:
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o mytemplate.o mytemplate.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++ -Wall -Wextra -std=c++11 -pedantic-errors -o main.out main.o mytemplate.o
Production:
2
Testé dans Ubuntu 18.04.
Modules C ++ 20
https://en.cppreference.com/w/cpp/language/modules
Je pense que cette fonctionnalité fournira la meilleure configuration à mesure qu'elle deviendra disponible, mais je ne l'ai pas encore vérifiée car elle n'est pas encore disponible sur mon GCC 9.2.1.
Vous devrez toujours faire une instanciation explicite pour obtenir l'accélération / la sauvegarde du disque, mais au moins nous aurons une solution sensée pour "Supprimer les définitions des en-têtes inclus mais aussi exposer les modèles d'une API externe" qui ne nécessite pas de copier les choses environ 100 fois.
Utilisation attendue (sans l'insantiation explicite, vous ne savez pas à quoi ressemblera la syntaxe exacte, voir: Comment utiliser l'instanciation explicite de modèle avec des modules C ++ 20? )
helloworld.cpp
export module helloworld;
import <iostream>;
template<class T>
export void hello(T t) {
std::cout << t << std::end;
}
main.cpp
import helloworld;
int main() {
hello(1);
hello("world");
}
puis compilation mentionnée sur https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/
clang++ -std=c++2a -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm
clang++ -std=c++2a -c -o helloworld.o helloworld.cpp
clang++ -std=c++2a -fprebuilt-module-path=. -o main.out main.cpp helloworld.o
Donc, à partir de cela, nous voyons que clang peut extraire l'interface du modèle + l'implémentation dans la magie helloworld.pcm
, qui doit contenir une représentation intermédiaire LLVM de la source: Comment les modèles sont-ils gérés dans le système de modules C ++? ce qui permet toujours la spécification du modèle.
Comment analyser rapidement votre build pour voir s'il gagnerait beaucoup à l'instanciation de modèle
Donc, vous avez un projet complexe et vous voulez décider si l'instanciation du modèle apportera des gains significatifs sans faire le refactor complet?
L'analyse ci-dessous peut vous aider à décider, ou du moins à sélectionner les objets les plus prometteurs à refactoriser en premier pendant que vous expérimentez, en empruntant quelques idées à: Mon fichier objet C ++ est trop gros
# List all weak symbols with size only, no address.
find . -name '*.o' | xargs -I{} nm -C --size-sort --radix d '{}' |
grep ' W ' > nm.log
# Sort by symbol size.
sort -k1 -n nm.log -o nm.sort.log
# Get a repetition count.
uniq -c nm.sort.log > nm.uniq.log
# Find the most repeated/largest objects.
sort -k1,2 -n nm.uniq.log -o nm.uniq.sort.log
# Find the objects that would give you the most gain after refactor.
# This gain is calculated as "(n_occurences - 1) * size" which is
# the size you would gain for keeping just a single instance.
# If you are going to refactor anything, you should start with the ones
# at the bottom of this list.
awk '{gain = ($1 - 1) * $2; print gain, $0}' nm.uniq.sort.log |
sort -k1 -n > nm.gains.log
# Total gain if you refactored everything.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.gains.log
# Total size. The closer total gain above is to total size, the more
# you would gain from the refactor.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.log
Le rêve: un cache de compilateur de modèles
Je pense que la solution ultime serait de pouvoir construire avec:
g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp
puis myfile.o
réutiliserait automatiquement les modèles précédemment compilés dans les fichiers.
Cela signifierait 0 effort supplémentaire sur les programmeurs en plus de transmettre cette option CLI supplémentaire à votre système de construction.
Un bonus secondaire de l'instanciation de modèle explicite: aidez les IDE à répertorier les instanciations de modèle
J'ai constaté que certains IDE tels qu'Eclipse ne peuvent pas résoudre "une liste de toutes les instanciations de modèles utilisées".
Ainsi, par exemple, si vous êtes à l'intérieur d'un code basé sur un modèle et que vous voulez trouver les valeurs possibles du modèle, vous devrez trouver les utilisations du constructeur un par un et en déduire les types possibles un par un.
Mais sur Eclipse 2020-03, je peux facilement lister les modèles explicitement instanciés en effectuant une recherche Find all usages (Ctrl + Alt + G) sur le nom de la classe, ce qui me pointe par exemple à partir de:
template <class T>
struct AnimalTemplate {
T animal;
AnimalTemplate(T animal) : animal(animal) {}
std::string noise() {
return animal.noise();
}
};
à:
template class AnimalTemplate<Dog>;
Voici une démo: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15
Une autre technique de guérilla que vous pourriez utiliser en dehors de l'EDI serait de s'exécuter nm -C
sur l'exécutable final et de grep le nom du modèle:
nm -C main.out | grep AnimalTemplate
ce qui indique directement que Dog
c'était l'une des instanciations:
0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)