16 juillet 20265 min de lecture #optimization #meson #clang #pgo #lto #cpp #build-systems

Comment utiliser PGO et LTO avec Meson et Clang

Un guide pas à pas testé pour activer le Profile Guided Optimization et le Link Time Optimization dans un projet Meson compilé avec Clang, y compris l'étape de fusion llvm-profdata que la plupart des guides oublient.

J’ai expliqué pourquoi PGO et LTO n’ont presque rien changé sur ma simulation SPH dans la partie 2 de ma série sur l’optimisation. Cet article laisse de côté la question « est-ce que ça aide vraiment » et se contente de vous donner les commandes, parce que la moitié des guides que j’ai trouvés en ligne passent sous silence une étape qui fera échouer votre build avec une erreur clang obscure.

La version courte #

PGO (Profile Guided Optimization) exécute une version instrumentée de votre programme sur des entrées réelles, enregistre quelles branches et fonctions sont effectivement exécutées, puis réinjecte ces données dans une seconde compilation pour que le compilateur puisse organiser les chemins chauds/froids et inliner en fonction de preuves plutôt que de suppositions.

LTO (Link Time Optimization) permet au compilateur d’optimiser à travers les unités de compilation plutôt qu’un seul fichier .cpp à la fois, ce qui lui permet d’inliner à travers les fichiers, d’éliminer le code mort, et de voir à travers des appels qui seraient autrement opaques au niveau du fichier objet.

Ils sont orthogonaux. Vous pouvez utiliser l’un ou l’autre séparément, ou cumuler les deux pour le build de production final.

Prérequis #

  • Clang. J’utilise clang 22 ici, mais tout ce qui date des dernières années fonctionne très bien.
  • Meson 0.57 ou plus récent si vous voulez le thin LTO (b_lto_mode).
  • lld n’est pas obligatoire, mais il simplifie la mise en cache du thin LTO. GNU ld et gold prennent tous les deux en charge le LTO via l’interface plugin, c’est juste plus de rouages.

Activer LTO #

text
meson setup build --buildtype=release -Db_lto=true
meson compile -C build

C’est le LTO complet : chaque unité de compilation est regroupée en un seul module géant au moment du lien. C’est exhaustif, et c’est lent, à la fois en temps de lien et en RAM utilisée par le linker. Sur un gros projet, ça peut faire mal.

Le thin LTO résout ça en conservant l’optimisation par module tout en partageant des résumés entre les modules, ce qui permet au linker de paralléliser l’étape de génération de code :

text
meson setup build --buildtype=release -Db_lto=true -Db_lto_mode=thin -Db_lto_threads=8
meson compile -C build

Si vous voulez confirmer que meson transmet bien le flag et ne l’ignore pas silencieusement, passez -v à votre build ninja :

text
ninja -C build -v | grep -o '\-flto[^ ]*'
-flto=thin

Vous voulez spécifiquement lld ? Définissez-le avant meson setup, pas comme option -D :

text
CXX_LD=lld meson setup build --buildtype=release -Db_lto=true -Db_lto_mode=thin

Activer PGO #

C’est la partie en deux passes.

Passe 1 : instrumenter et exécuter #

text
meson setup build-pgo --buildtype=release -Db_pgo=generate
meson compile -C build-pgo
./build-pgo/your-binary --some-representative-workload

Exécuter le binaire instrumenté dépose un fichier à côté de l’endroit d’où vous l’avez lancé, nommé quelque chose comme default_15853201381332895877_0.profraw. Pas dans le répertoire de build. Là où se trouvait votre shell actuel au moment où vous avez lancé le binaire.

Passe 2 : utiliser les données profilées #

Meson ne fusionne pas le profil pour vous. Il n’y a aucune chaîne profdata ou profraw nulle part dans le code source de mesonbuild. Si vous reconfigurez simplement avec -Db_pgo=use et compilez, vous obtenez ceci :

text
clang: error: Error in reading profile default.profdata: No such file or directory
ninja: build stopped: subcommand failed.

Clang veut un default.profdata déjà fusionné et indexé, et il le cherche dans son propre répertoire de travail, qui, pour le backend ninja, est la racine de votre répertoire de build. Alors fusionnez-le vous-même à cet endroit :

text
llvm-profdata merge -output=build-pgo/default.profdata *.profraw

Ensuite reconfigurez et recompilez :

text
meson configure build-pgo -Db_pgo=use
meson compile -C build-pgo

Si vous avez plusieurs charges de travail représentatives, exécutez le binaire instrumenté une fois par charge de travail et donnez tous les fichiers .profraw résultants au même appel llvm-profdata merge. Plus de couverture, un seul profil.

Ne livrez jamais le build instrumenté. Les binaires -Db_pgo=generate paient le prix de compteurs sur chaque branche et chaque appel de fonction, et ce n’est pas une erreur d’arrondi.

Les deux en même temps #

Pour un vrai build de release, générez d’abord le profil sur un build optimisé simple (itération plus rapide, et LTO ne vous aide pas à trouver les chemins chauds, il agit seulement dessus), puis effectuez le build final avec les deux flags ensemble :

text
meson configure build-pgo -Db_pgo=use -Db_lto=true -Db_lto_mode=thin
meson compile -C build-pgo

Mises en garde #

  • Profils périmés. Si vous modifiez le code au point que le flux de contrôle d’une fonction ne correspond plus à ce qui a été profilé, clang détecte la différence de hash et abandonne silencieusement le profil de cette fonction au lieu de faire échouer le build. Pratique, mais cela signifie qu’un profil peut devenir périmé sans vous prévenir.
  • Dérive de version du compilateur. Le format brut du profil est lié à la version de clang qui l’a produit. Générez-le avec un clang et essayez de l’utiliser avec un autre, et vous obtiendrez une incompatibilité de version au lieu d’un build.
  • Consommation mémoire du LTO complet. Si votre linker commence à swapper sur un gros projet, c’est le LTO complet qui garde tout le programme en mémoire en une fois. Passez à b_lto_mode=thin avant de lui balancer plus de RAM.
  • L’étape de fusion, encore une fois. Je le répète parce que c’est celle qui bloque vraiment votre build : pas de default.profdata fusionné dans le répertoire de build signifie pas de build -Db_pgo=use.

Si vous vous demandez si tout cela vaut la complexité de build supplémentaire pour votre cas d’usage, c’est justement le sujet de la partie 2 : sur une boucle numérique avec peu de branches, ça m’a fait gagner environ 3 %. Les résultats varieront selon le nombre de branches que contient réellement votre chemin chaud.

Plus d'articles

Partie 2 : Profiling, PGO & LTO

Mesurer PGO et LTO sur une simulation SPH en C++ avec perf et des flamegraphs, et pourquoi ils n'ont presque rien apporté.

8 min