# 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](/posts/optimizing-low-level/part-2-profiling). 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

```
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 :

```
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 :

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

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

```
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

```
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 :

```
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 :

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

*Ensuite* reconfigurez et recompilez :

```
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.

{{< alert >}}
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.
{{< /alert >}}

# 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 :

```
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](/posts/optimizing-low-level/part-2-profiling) : 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.

