- Le projet CPython a récemment introduit une nouvelle stratégie d’implémentation pour l’interpréteur de bytecode. Les premiers résultats montraient un gain de performance moyen de 10 à 15 % sur diverses plateformes
- Cependant, ce gain de performance provenait surtout d’un contournement d’une régression de LLVM 19. Avec de meilleures références de comparaison (par ex. GCC, clang-18, LLVM 19 avec certains drapeaux d’ajustement), le gain retombe à 1-5 %
Résultats de performance
- Plusieurs builds de l’interpréteur CPython ont été benchmarkés avec différents compilateurs et options de configuration. Les tests ont été effectués sur un serveur Intel et un Macbook Air Apple M1.
- Tous les builds utilisaient LTO et PGO. La moyenne rapportée par
pypeformance/pyperf compare_to a été utilisée avec clang18 comme référence.
- Comparaison des performances des compilateurs
- Macbook Air Apple M1 :
- clang18 : référence
- clang19 : 1,12x plus lent
- clang19.taildup : 1,02x plus lent
- clang19.tc : 1,00x plus lent
- gcc : N/A
- L’interpréteur à appels de queue montrait toujours une amélioration de vitesse par rapport à clang-18, mais la baisse de performance lors du passage à clang-19 était encore plus marquée.
Régression LLVM
Bref contexte
- Les interpréteurs de bytecode traditionnels sont composés d’une instruction
switch à l’intérieur d’une boucle while. La plupart des compilateurs compilent le switch en table de saut.
- Les compilateurs C modernes prennent en charge le motif consistant à prendre l’adresse d’un label et à l’utiliser comme un "goto calculé". CPython utilisait ce motif jusqu’au travail sur les appels de queue.
Régression LLVM 19
- LLVM 19 a imposé une limite au passage de tail-duplication, en arrêtant la duplication lorsque la taille de l’IR dépassait un certain seuil. Dans CPython, cela a eu pour effet de fusionner tous les sauts de dispatch, annulant complètement l’objectif de l’implémentation fondée sur le
goto calculé.
Autres anomalies
- Il est certain que la modification de la logique de duplication des appels de queue a provoqué la régression, mais cela n’explique pas complètement l’ampleur de la régression.
- Sur les processeurs modernes, un gain de vitesse de 2 à 4 % est plus courant.
Le goto calculé est-il nécessaire ?
- Le benchmark
clang19.nocg affirme être plus rapide que clang19. Cela montre qu’un compilateur peut appliquer les mêmes optimisations avec un interpréteur basé sur switch.
Correctif
- La pull request LLVM 114990 a corrigé la régression. Ce correctif rétablit les performances attendues.
Réflexions
À propos du benchmarking
- Lors de l’optimisation de systèmes, on construit des benchmarks et une méthodologie de benchmarking, puis on évalue les changements proposés.
- Les benchmarks exigent davantage d’hypothèses et de confiance pour généraliser un point de données donné.
Référence de base
- Lorsqu’on propose une nouvelle solution ou une nouvelle méthode, il est courant de la comparer à "la meilleure approche actuellement connue".
À propos du génie logiciel
- Les systèmes logiciels sont complexes, interconnectés et évoluent rapidement.
- Les compilateurs optimisants sont dans une relation de tension : ils doivent optimiser le code tout en respectant l’intention du programmeur.
Compilateurs optimisants
- L’attribut
musttail représente un nouveau type de fonctionnalité de compilateur liée à l’optimisation. Il pourrait offrir un style plus puissant pour écrire du code sensible aux performances.
Encore un mot sur nix
nix a été très utile dans ce projet. Il a grandement aidé à gérer et compiler plusieurs versions de l’interpréteur Python.
1 commentaires
Avis sur Hacker News
Bonjour. Je suis l’auteur de la PR qui a introduit l’interpréteur à tail-calls dans CPython
Le benchmarking est vraiment un exercice très difficile
Je salue l’auteur pour avoir mis au jour la vérité sur ce problème
Voilà un bon exemple montrant que le C n’est pas un langage « proche de la machine »
gotocalculé, mais génère une sortie complètement différente de l’intention d’optimisationswitch()En ajustant la manière dont le compilateur organise la boucle, l’interpréteur à tail-calls n’est pas aussi efficace qu’annoncé
Évaluer les performances d’un build Python est très difficile
Discussion connexe :
Excellent article
J’ai récemment fait du benchmarking de Python 3.9 à 3.13
Je me demande comment ce type d’optimisation est lié à l’optimisation des tail-calls