Admettons qu'un certain projet utilise beaucoup de modèles (alias templates) et mette beaucoup de temps à compiler en partie à cause de cela.
Le projet contient des entêtes qui contiennent à chaque fois la définition des modèles qu'il déclare.
Pour réduire le temps de compilation, quand une unité de compilation utilise une instance de modèle de fonction (par exemple foo<int>()) ou une fonction d'une instance d'un modèle de classe (par exemple Toto<int>::bar()) :
-On aimerait éviter d'instancier la définition de foo<int>() ou de Toto<int>::bar() (ce que permet extern template) si cette dernière est déjà instanciée dans une autre unité de compilation.
-Alors, on aimerait éviter que cette unité de compilation inclut des choses qui ne servent qu'à compiler foo<int>() ou Toto<int>::bar().
Je propose la procédure suivante :
Étape 1 : Tripler des entêtes
L'entête "NomFichier.h" devient les 3 entêtes suivants :
- NomFichier_light.h
Permet de déclarer un ou plusieurs modèles, sans forcément les définir. Néanmoins, pour la même raison qu'une classe non template a parfois des fonctions en ligne, "NomFichier_light.h" peut avoir quelques définitions.- NomFichier.h
Permet d'inclure toutes les définitions du ou des modèles déclarés directement dans "NomFichier_light.h", ainsi que les définitions des éventuels autres modèles qu'ils utilisent : "NomFichier.h" inclut parfois "AutreNomFichier.h" mais jamais "AutreNomFichier_light.h".- NomFichier_timpl.h (timpl comme template implementation)
Permet d'inclure toutes les définitions du ou des modèles déclarés directement dans "NomFichier_light.h". Néanmoins, quand c'est possible, on évite d'inclure les définitions des éventuels autres modèles qu'ils utilisent : "NomFichier_timpl.h" préfère inclure "AutreNomFichier_light.h" à "AutreNomFichier.h".
Remarque : Pour certains projets, on pourrait supprimer "NomFichier_timpl.h" pour simplifier la procédure. Les entêtes seraient alors seulement doublés.
Étape 2 : Isoler des instanciations dans des unités de compilation spécifiques
Par exemple, admettons qu'un fichier "Zoo.cpp" utilise le modèle de classe Toto<int> :
- On crée le fichier "Toto_tinst__int.cpp" qui instancie Toto<int> (tinst comme template instantiation).
- Dans "Zoo.cpp", on remplace #include "Toto.h" par #include "Toto_light.h" et on ajoute un commentaire pour dire qu'il faut inclure au projet "Toto_tinst__int.cpp".
- "Toto_tinst__int.cpp" contient soit #include "Toto.h", soit, quand cela permet de réduire le temps de compilation, #include "Toto_timpl.h". Dans le 2e cas, il faut ajouter un commentaire dans "Toto_tinst__int.cpp" pour signaler quel(s) autre(s) fichier(s) ".cpp" il faut inclure au projet pour que ça compile.
Exemple de résultat de la procédure :
Foo est une classe très simple qui sera utilisée plus loin.
Utils :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 ////////// // Foo.h : #ifndef INCLUDE_FAST_TEMPLATE_COMPILATION_PROJECT_FOO_H #define INCLUDE_FAST_TEMPLATE_COMPILATION_PROJECT_FOO_H #include <iosfwd> class Foo { }; std::ostream& operator<<(std::ostream& os, const Foo& foo); std::wostream& operator<<(std::wostream& os, const Foo& foo); #endif //////////// // Foo.cpp #include "Foo.h" #include <ostream> std::ostream& operator<<(std::ostream& os, const Foo& foo) { os << "ASCII foo\n"; return os; } std::wostream& operator<<(std::wostream& os, const Foo& foo) { os << L"wide foo\n"; return os; }
- Utils est un espace de nom qui contient une fonction normale et un modèle de fonction print.
- Il n'y a pas de distinction entre "Utils.h" et "Utils_timpl.h".
- "Utils_tinst__charAndVectorFoo.cpp" instancie la définition de Utils::print<char, std::vector<Foo>>.
FooContainerPrinter :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66 //////////////// // Utils_light.h #ifndef INCLUDE_FAST_TEMPLATE_COMPILATION_PROJECT_UTILS_LIGHT_H #define INCLUDE_FAST_TEMPLATE_COMPILATION_PROJECT_UTILS_LIGHT_H #include <iosfwd> namespace Utils { void nonTemplateFunction(); template<class CharT, class ContainerT> void print(std::basic_ostream<CharT>& os, const ContainerT& container); } #endif //////////// // Utils.cpp #include "Utils_light.h" namespace Utils { void nonTemplateFunction() { // code... } } //////////////// // Utils_timpl.h #ifndef INCLUDE_FAST_TEMPLATE_COMPILATION_PROJECT_UTILS_TIMPL_H #define INCLUDE_FAST_TEMPLATE_COMPILATION_PROJECT_UTILS_TIMPL_H #include "Utils_light.h" #include <ostream> namespace Utils { template<class CharT, class ContainerT> void print(std::basic_ostream<CharT>& os, const ContainerT& container) { for(const auto& element : container) os << element; } } #endif ////////// // Utils.h #include "Utils_timpl.h" //////////////////////////////////// // Utils_tinst__charAndVectorFoo.cpp #include "Utils.h" #include "Foo.h" #include <vector> template void Utils::print<char, std::vector<Foo>>( std::ostream& os, const std::vector<Foo>& container);
- FooContainerPrinter est un modèle de classe qui utilise Foo et Utils.
- Il y a une distinction entre "FooContainerPrinter.h" et "FooContainerPrinter_timpl.h" : "FooContainerPrinter.h" inclut "Utils.h". La conséquence est illustrée dans les deux points suivants.
- "FooContainerPrinter_tinst__charAndVector.cpp" inclut "FooContainerPrinter_timpl.h" mais a besoin que le projet contienne "Utils_tinst__charAndVectorFoo.cpp" qui instancie la définition de Utils::print<char, std::vector<Foo>>.
- "FooContainerPrinter_tinst__wchar_tAndDeque.cpp" inclut "FooContainerPrinter.h" qui est plus lourd que "FooContainerPrinter_timpl.h", mais c'est nécessaire car il faut instancier la définition de Utils::print<wchar_t, std::deque<Foo>> et aucun autre ".cpp" le fait.
Exemple d'utilisation :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86 ////////////////////////////// // FooContainerPrinter_light.h #ifndef INCLUDE_FAST_TEMPLATE_COMPILATION_PROJECT_FOO_CONTAINER_PRINTER_LIGHT_H #define INCLUDE_FAST_TEMPLATE_COMPILATION_PROJECT_FOO_CONTAINER_PRINTER_LIGHT_H #include <iosfwd> #include <type_traits> class Foo; template<class CharT> class FooContainerPrinter { public: inline explicit FooContainerPrinter(std::basic_ostream<CharT>& os) : m_os(os) {} template<class ContainerT> inline void print(const ContainerT& container) const { static_assert(std::is_same<Foo, typename ContainerT::value_type>::value, "The container argument must be a container of Foo."); printImpl(container); } // Inline implicit functions: // -destructor // -copy constructor // -move constructor private: template<class ContainerT> void printImpl(const ContainerT& container) const; std::basic_ostream<CharT>& m_os; }; #endif ////////////////////////////// // FooContainerPrinter_timpl.h #ifndef INCLUDE_FAST_TEMPLATE_COMPILATION_PROJECT_FOO_CONTAINER_PRINTER_TIMPL_H #define INCLUDE_FAST_TEMPLATE_COMPILATION_PROJECT_FOO_CONTAINER_PRINTER_TIMPL_H #include "FooContainerPrinter_light.h" #include "Utils_light.h" template<class CharT> template<class ContainerT> void FooContainerPrinter<CharT>::printImpl(const ContainerT& container) const { Utils::print(m_os, container); } #endif //////////////////////// // FooContainerPrinter.h #include "FooContainerPrinter_timpl.h" #include "Utils.h" /////////////////////////////////////////////// // FooContainerPrinter_tinst__charAndVector.cpp // Remark: "Utils_tinst__charAndVectorFoo.cpp" must be added to the project. #include "FooContainerPrinter_timpl.h" #include "Foo.h" #include <vector> template class FooContainerPrinter<char>; template void FooContainerPrinter<char>::printImpl<std::vector<Foo>>(const std::vector<Foo>& container) const; template void FooContainerPrinter<char>::print <std::vector<Foo>>(const std::vector<Foo>& container) const; ///////////////////////////////////////////////// // FooContainerPrinter_tinst__wchar_tAndDeque.cpp #include "FooContainerPrinter.h" #include "Foo.h" #include <deque> template class FooContainerPrinter<wchar_t>; template void FooContainerPrinter<wchar_t>::printImpl<std::deque<Foo>>(const std::deque<Foo>& container) const; template void FooContainerPrinter<wchar_t>::print <std::deque<Foo>>(const std::deque<Foo>& container) const;
On garde la rétrocompatibilité avec la méthode consistant à inclure "FooContainerPrinter.h" sans instancier les définitions de FooContainerPrinter::print dans d'autres ".cpp" :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 ///////////////////////// // WithLightInclusion.cpp // Remark: "FooContainerPrinter_tinst__charAndVector.cpp" must be added to the project. // Remark: "FooContainerPrinter_tinst__wchar_tAndDeque.cpp" must be added to the project. #include "Foo.h" #include "FooContainerPrinter_light.h" #include <deque> #include <iostream> #include <vector> namespace WithLightInclusion { void doSomething() { std::cout << "WithLightInclusion::doSomething() :\n"; std::vector<Foo> vectorFoo(2); FooContainerPrinter<char> fooChar(std::cout); fooChar.print(vectorFoo); std::deque<Foo> dequeFoo(3); FooContainerPrinter<wchar_t> fooWchar_t(std::wcout); fooWchar_t.print(dequeFoo); } };
Remarque à propos des extern template
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 ///////////////////////// // WithUsualInclusion.cpp #include "Foo.h" #include "FooContainerPrinter.h" #include <array> #include <iostream> #include <list> namespace WithUsualInclusion { void doSomething() { std::cout << "WithUsualInclusion::doSomething() :\n"; std::list<Foo> listFoo; listFoo.push_back(Foo()); listFoo.push_back(Foo()); FooContainerPrinter<char> fooChar(std::cout); fooChar.print(listFoo); std::array<Foo, 3> arrayFoo; FooContainerPrinter<wchar_t> fooWchar_t(std::wcout); fooWchar_t.print(arrayFoo); } };
Au lieu de tripler les entêtes, on aurait pu utiliser les extern template pour éviter d'instancier plusieurs fois une même définition avec les mêmes types, mais :
- Ce ne sont pas les extern template qui permettront d'inclure des entêtes n'ayant que le strict minimum.
- Les extern template empêchent l'inlining, contrairement à la procédure que je propose dans laquelle on peut laisser quelques définitions dans "NomFichier_light.h".
- Les ".cpp" utilisant les extern template auraient été plus lourds à écrire et à maintenir : voir exemple ci-dessous.
Voici ce que ça donne avec mon exemple :
Bilan
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 ///////////////////////// // WithExternTemplate.cpp // Remark: "FooContainerPrinter_tinst__charAndVector.cpp" must be added to the project. // Remark: "FooContainerPrinter_tinst__wchar_tAndDeque.cpp" must be added to the project. #include "Foo.h" #include "FooContainerPrinter.h" #include <deque> #include <iostream> #include <vector> extern template void Utils::print<char, std::vector<Foo>>( std::ostream& os, const std::vector<Foo>& container); extern template class FooContainerPrinter<char>; extern template void FooContainerPrinter<char>::printImpl<std::vector<Foo>>(const std::vector<Foo>& container) const; extern template void FooContainerPrinter<char>::print <std::vector<Foo>>(const std::vector<Foo>& container) const; extern template class FooContainerPrinter<wchar_t>; extern template void FooContainerPrinter<wchar_t>::printImpl<std::deque<Foo>>(const std::deque<Foo>& container) const; extern template void FooContainerPrinter<wchar_t>::print <std::deque<Foo>>(const std::deque<Foo>& container) const; namespace WithExternTemplate { void doSomething() { std::cout << "WithExternTemplate::doSomething() :\n"; std::vector<Foo> vectorFoo(2); FooContainerPrinter<char> fooChar(std::cout); fooChar.print(vectorFoo); std::deque<Foo> dequeFoo(3); FooContainerPrinter<wchar_t> fooWchar_t(std::wcout); fooWchar_t.print(dequeFoo); } };
La procédure que je propose devrait réduire le temps de compilation pour les modèles qui ont des définitions lourdes qui sont instanciées avec les mêmes types dans plein d'unités de compilation du même projet.
Mais cette procédure me semble assez lourde à mettre en place et rendra le code plus difficile à comprendre (donc plus difficile à maintenir) pour les développeurs ne connaissant pas cette procédure.
Donc, à mon avis, ce serait à utiliser en dernier recours, après les autres méthodes pour réduire le temps de compilation (remplacer des #include par des déclarations en avance, utiliser pimpl...).
Auriez-vous des améliorations à suggérer pour cette procédure ?
Partager