mardi 17 mars 2015

Temps de (pré) compilation et boost::lexical_cast

Je garde un œil vigilant sur mes temps de compilation. En particulier, pour optimiser la compilation distribuée par distcc (je n'arrive pas à faire fonctionner le mode pump avec ma solution), il faut diminuer autant que faire ce peut le temps de pré-compilation, c'est à dire la phase où le compilateur va remplacer les directives #include par leur contenu, et évaluer les #if, #ifdef et autres. En effet, distcc agrège les différents en-têtes localement (car tous les fichiers sont disponibles), et distribue le (gros) fichier ainsi généré pour la phase de compilation proprement dite. L'édition de liens se fait également localement.

Or, je me suis vite rendu compte que la phase de pré-compilation prenait un temps considérable. Jusqu'à 30 secondes par fichier! Le client local passait donc sa vie à pré-compiler, alors que les CPUs distants se tournaient les pouces. Au final, la compilation entièrement locale, c'est à dire sans distcc, prenait à peu près autant de temps, car le compilateur peut optimiser la phase de pré-compilation et la phase de compilation lorsqu'elles sont faites ensemble.

Je n'ai pas encore résolu le problème plus général, mais en fouillant pour comprendre ce qui prenait autant de temps, j'ai découvert qu'un fichier d'en-tête était particulièrement coûteux: <boost/lexical_cast.hpp>.

Testez ce programme tout bête:

// lex.cpp
#include <boost/lexical_cast.hpp>

Compilez le:

time g++ -E lex.cpp -o lex.E

Sur ma machine, c'est 0.7 secondes passées juste à pré-compiler le fichier! Par comparaison, si je remplace l'inclusion par l'inclusion de l'en-tête standard pour les chaines de caractères, <string>, la phase de pré-compilation prend juste 0.025s. Alors, 0.7 secondes, ce n'est pas énorme, mais multiplié par des dizaines de milliers de fichiers, c'est non-négligeable. À noter que le temps a triplé entre boost 1.42 et boost 1.55. Cela se voit également à la taille du fichier ainsi généré: 20KLOC pour boost 1.42 contre 60KLOC pour boost 1.55.

Pour tenter de récupérer ce temps, il faut donc éliminer le plus possible les inclusions de boost::lexical_cast dans les en-têtes. Pour ce faire, il convient de remplacer les utilisations triviales par leurs équivalents moins flexibles mais souvent plus performants, et nettement plus rapides à compiler: std::to_string pour transformer les nombres en chaines, et std::stoi, std::stol, std::stod et autres pour transformer les chaines en nombres.

Pour les utilisations non-triviales, il faudra soit écrire un peu de code template pour remplacer le boost::lexical_cast par un équivalent plus simple, soit le pousser, si possible, dans le .cpp, où il n'embêtera personne d'autre.

Une fois débarassé de mes lexical_cast, je partirai à la recherche des autres mammouths, et je vous tiens au jus.

Aucun commentaire: