Quand le compilateur vous surprend
(xania.org)- Exemple observé lors de la compilation d’une simple fonction de somme d’entiers, mettant en évidence les différences d’optimisation entre GCC et Clang
- GCC effectue dans la boucle une optimisation de type
x*2 + 1, en additionnant deux nombres à la fois - Clang supprime complètement la boucle et remplace le calcul par une forme fermée
v(v - 1)/2 - Cette transformation fait passer le code de O(n) à O(1), avec une simplification mathématique réalisée automatiquement
- Même pour l’auteur, qui travaille avec les compilateurs depuis plus de 20 ans, c’est un exemple marquant du niveau d’optimisation intelligent des compilateurs
Optimisation de boucle dans GCC
- Lorsqu’on écrit une simple fonction de somme d’entiers, GCC génère un code de sommation efficace basé sur une boucle
- Dans la boucle, l’instruction
lea edx, [rdx+1+rax*2]est utilisée pour additionner deux nombres simultanément - Cela revient à transformer l’opération consistant à additionner
xetx+1enx*2 + 1
- Dans la boucle, l’instruction
- En poussant le niveau d’optimisation à
-O3, la boucle est traitée plus rapidement grâce à l’addition parallèle (vectorization) - Cette approche est une forme d’optimisation traditionnelle qui maximise l’efficacité des opérations tout en conservant la boucle
Optimisation mathématique dans Clang
- En compilant le même code avec Clang, la boucle disparaît complètement
- Clang vérifie d’abord si la valeur d’entrée est positive, puis effectue le calcul à l’aide d’une série d’instructions
lea,imuletshr - Au final, le calcul est transformé en
(v² - v)/2, c’est-à-dire en forme fermée de la somme d’entiers
- Clang vérifie d’abord si la valeur d’entrée est positive, puis effectue le calcul à l’aide d’une série d’instructions
- Cette transformation élimine la boucle et la remplace par un calcul en temps constant (O(1))
- L’auteur décrit ce résultat comme le fait d’avoir « trouvé par lui-même la solution mathématique de la somme d’entiers »
Développement de la formule
- En retraçant mathématiquement le code assembleur généré par Clang, on obtient la transformation suivante
v + ((v - 1)(v - 2) / 2) - 1- En développant et en simplifiant, on obtient
(v² - v)/2
- Au final, on arrive à la forme
v(v - 1)/2, qui correspond à la formule de la somme de 1 à v - Ce processus est présenté comme un exemple d’optimisation où le compilateur reconnaît un motif mathématique
Fonctionnement intelligent du compilateur
- La raison pour laquelle Clang utilise cette séquence d’instructions particulière est expliquée par des considérations de prévention des débordements et de suivi des variables d’induction
- Même si la cause exacte de son fonctionnement interne n’est pas parfaitement claire, le résultat est attribué à la logique d’optimisation interne complexe de Clang
- L’auteur qualifie cette découverte d’« expérience à la fois humble et inspirante »
Conclusion et contexte de la série
- Cet article est le 24e billet de la série « Advent of Compiler Optimisations 2025 »
- Il explore la manière dont les compilateurs parviennent, par transformation du code, à simplifier mathématiquement les calculs et améliorer les performances
- L’auteur souligne que « les compilateurs continuent de me surprendre », mettant en avant cet émerveillement technique qui perdure malgré une longue expérience
1 commentaires
Commentaires sur Hacker News
Le code qui implémente cette fonctionnalité se trouve dans ScalarEvolution.cpp et IndVarSimplify.cpp
Ce genre d’optimisation est vraiment intéressant
Les optimisations de compilateur se divisent globalement en deux catégories — analyse de flux de données et reconnaissance de motifs avec substitution
La première est efficace pour la plupart des programmes, tandis que la seconde consiste à accumuler des motifs avec un rendement de plus en plus faible
L’exemple lié est intelligent et amusant, mais en pratique il n’a pas beaucoup de valeur. En 45 ans, je n’ai jamais écrit une boucle de ce type
Bien sûr, ce genre de substitution de motifs reste important, car il est généré automatiquement à partir de code de haut niveau
Pour être honnête, c’est peut-être juste moi qui râle un peu parce que mon optimizer n’est pas capable de faire ce genre d’optimisation
Il s’agit d’une fonctionnalité appelée Scalar Evolution (SCEV), et son implémentation dans LLVM est assez complexe
Il existe un cas d’optimisation similaire dans l’article « Do not optimize away »
Ce qu’il y a de vraiment remarquable dans cette optimisation, c’est qu’elle est générique
Ce qui impressionne, ce n’est pas simplement qu’elle reconnaisse le motif « somme d’une suite entière finie », mais qu’elle s’applique de façon générale
J’ai l’impression que le compilateur pourrait ajouter davantage de closed forms. Je me demande si cela en vaudrait la peine
Il y a un concept connexe avec les paires de Wilf–Zeilberger
Une fois de plus, j’ai été surpris par un résultat où GCC est plus lent que Clang
Même avec 20 ans d’avance, il arrive encore que GCC produise un code plus lent que Clang
Autrefois, lors d’une extraction de bits, Clang s’en sortait avec deux décalages, alors que GCC en faisait trois
Je me demande si quelqu’un a déjà tenté une optimisation qui remplace un problème de coloration de graphe (graph coloring) par une constante
Voilà un exemple qui brouille la frontière entre implémentation et spécification
Nous pensons écrire une implémentation, mais en réalité nous écrivons une sorte de proxy de la spécification
Autrement dit, le compilateur crée l’illusion d’une machine impérative
Au début, j’étais surpris que Matt ne connaisse pas ce comportement
Moi aussi, à l’université, en jouant avec LLVM IR, j’avais été choqué de voir une récursion se simplifier en multiplication
Le fait que Matt ne le sache pas pourrait vouloir dire que cette optimisation ne s’applique qu’à des cas simples qu’il ne rencontre pas
Lien vers la vidéo YouTube