Performances de l’interpréteur à appels terminaux de Python 3.14
(blog.nelhage.com)- Le nouvel interpréteur à appels terminaux de CPython semblait d’abord apporter une amélioration moyenne de 10 à 15 % sur pyperformance, mais une fois la base de référence ajustée, le gain réel se resserre à environ 1 à 5 % selon la configuration
- Les gains importants tenaient moins au seul effet de la nouvelle implémentation qu’au contournement d’une régression de LLVM 19 ; le facteur clé était l’incapacité de Clang 19 à répliquer correctement le dispatch existant basé sur computed goto
- Sur un Intel Raptor Lake i5-13500, la build clang19 était 1,09× plus lente que clang18 et clang19.tc 1,03× plus rapide, mais sur un MacBook Air Apple M1, clang19 était 1,12× plus lent et clang19.tc restait à 1,00× plus lent
- Avec la limitation de tail duplication de LLVM, le nombre de sauts indirects est passé de 332 avec clang18 à 3 avec clang19, ce qui a fait pratiquement disparaître la structure voulue de l’interpréteur basé sur computed goto
- L’approche par appels terminaux reste une amélioration significative, mais une approche qui exige explicitement une optimisation du compilateur, comme
musttail, peut être plus robuste pour du code sensible aux performances
Un effet de base de référence qui ressemblait à une amélioration des performances
- Le projet CPython a fusionné il y a environ un mois une nouvelle stratégie d’implémentation pour l’interpréteur de bytecode
- Les premiers résultats montraient en moyenne une amélioration des performances de 10 à 15 % sur diverses plateformes et avec les benchmarks pyperformance
- Des analyses ultérieures ont montré que ce gain important provenait surtout du contournement accidentel d’une régression de LLVM 19
- Par rapport à GCC, clang-18 ou LLVM 19 avec certains flags de tuning, le gain retombe à environ 1 à 5 %
- L’interpréteur à appels terminaux produit bien une réelle accélération, mais l’ampleur du gain est plus modérée que ne le suggéraient les premiers chiffres
- Si vous avez compilé avec clang-19 ou une version ultérieure, l’ancien chemin était peut-être réellement 10 à 15 % plus lent
- Simon Willison a reproduit un gain de 10 % par rapport aux builds
python-build-standaloneet Python 3.13
- Simon Willison a reproduit un gain de 10 % par rapport aux builds
Configuration des benchmarks et chiffres clés
- Plusieurs builds de CPython ont été comparées sur un serveur Intel et sur un MacBook Air Apple M1
- Le serveur Intel est un Raptor Lake i5-13500 opéré chez Hetzner
- Toutes les builds utilisent LTO et PGO
- Une configuration nix a été utilisée pour reproduire les builds
- Les configurations comparées sont les suivantes
clang18: Clang 18.1.8, computed gotogcc: GCC 14.2.1, computed goto, Intel uniquementclang19: Clang 19.1.7, computed gotoclang19.tc: Clang 19.1.7, nouvel interpréteur à appels terminauxclang19.taildup: Clang 19.1.7, computed goto et flag de tuning-mllvmpour contourner la régression
- Les résultats moyens de pyperformance, avec
clang18comme base de référence, sont les suivants- Raptor Lake i5-13500 :
clang19: 1,09× plus lentclang19.taildup: 1,01× plus rapideclang19.tc: 1,03× plus rapidegcc: 1,02× plus rapide
- MacBook Air Apple M1 :
clang19: 1,12× plus lentclang19.taildup: 1,02× plus lentclang19.tc: 1,00× plus lent
- Raptor Lake i5-13500 :
- L’interpréteur à appels terminaux a montré certains gains de vitesse par rapport à clang-18, mais ils étaient inférieurs à la baisse de performance introduite par le passage à clang-19
clang18.tcn’a pas pu être mesuré- L’interpréteur à appels terminaux dépend d’une fonctionnalité du compilateur nouvellement arrivée dans Clang 19
- À cause de cette contrainte, il fallait davantage de combinaisons de benchmarks pour comprendre la situation
L’effondrement du dispatch provoqué par la régression de LLVM 19
- Un interpréteur de bytecode traditionnel traite les opcodes avec une instruction
switchdans une bouclewhile- Le compilateur transforme généralement le
switchen table de sauts et en saut indirect
- Le compilateur transforme généralement le
- On sait depuis longtemps que répliquer la logique de dispatch dans le corps de chaque opcode peut accélérer ce type d’interpréteur
- Au lieu de revenir au début de la boucle à la fin de chaque opcode, la logique qui décode l’instruction suivante et indexe la table de sauts est placée séparément dans chaque opcode
- Les compilateurs C fournissent une fonctionnalité permettant de récupérer l’adresse d’un label et de l’utiliser comme computed goto, et CPython utilisait cette forme de boucle d’interpréteur avant le travail sur les appels terminaux
- Pour des raisons de performance du compilateur, Clang/LLVM fusionne en interne plusieurs
gotod’un computed goto en une seule instruction LLVM indirectbr- Ensuite, lors de la génération de code, il effectue une tail duplication pour répliquer de nouveau la logique de branchement à chaque emplacement
- Ce flux est documenté à haut niveau dans un ancien billet de blog de LLVM
- LLVM 19 a introduit une limitation de la passe de tail duplication pour éviter, dans certains cas, de fortes augmentations du temps de compilation ou de l’utilisation mémoire
- Dans CPython, cette limitation a conduit Clang à laisser les sauts de dispatch à l’état fusionné
- En conséquence, l’objectif de l’implémentation basée sur computed goto a été pratiquement neutralisé
- Ce problème avait d’abord été identifié dans d’autres implémentations de langages dotées de boucles d’interpréteur similaires, mais son impact sur CPython n’était pas connu
- En désassemblant le code objet et en comptant les sauts indirects, la différence apparaît directement
_PyEval_EvalFrameDefaultdans la build clang18 : 332 occurrences dejmp *_PyEval_EvalFrameDefaultdans la build clang19 : 3 occurrences dejmp *
La position ambiguë de computed goto
- Le fait que le changement de logique de tail duplication ait provoqué la régression est confirmé par le retour aux performances de clang-18 après correction
- En revanche, la taille de la régression n’est pas complètement expliquée
- Historiquement, la réplication du dispatch d’opcode a parfois été citée comme accélérant les interpréteurs de 20 % à 100 %
- Sur les processeurs modernes dotés de prédicteurs de branchement améliorés, des travaux plus récents montrent des gains plus modestes, de l’ordre de 2 à 4 %
- Python prend aussi en charge, via une option de configuration, un ancien interpréteur utilisant une unique instruction
switchclang18.nocg: 1,01× plus rapide que clang18clang19.nocg: 1,02× plus lent que clang18clang19: 1,09× plus lent que clang18
- Le fait que
clang19.nocgsoit plus rapide queclang19constitue un retournement supplémentaire- Clang 18, ou Clang 19 avec les flags appropriés, réplique la logique de dispatch dans le corps de chaque opcode même pour l’interpréteur basé sur
switch
- Clang 18, ou Clang 19 avec les flags appropriés, réplique la logique de dispatch dans le corps de chaque opcode même pour l’interpréteur basé sur
- La comparaison du nombre de sauts indirects montre aussi cette différence
clang18: 332clang18.nocg: 306clang19.nocg: 3clang19: 3
- Avec les versions modernes de Clang, l’ensemble de l’interpréteur computed goto pourrait être une complexité inutile
- Car le compilateur peut effectuer la même transformation même sur du code basé sur
switch - À l’inverse, computed goto lui-même ne suffisait pas à garantir la transformation
- Car le compilateur peut effectuer la même transformation même sur du code basé sur
- GCC 14.2.1 ne répliquait pas le
switch, mais implémentait bien le comportement attendu lorsque computed goto était utilisé
Correctif et contournement
- La pull request LLVM 114990 a été fusionnée peu après la publication du billet et corrige la régression
- Les benchmarks réalisés avant la fusion confirmaient déjà que ce correctif rétablissait les performances attendues
- Dans les versions publiées avant le correctif, l’option de tuning ajoutée par la PR qui a causé la régression permet d’ajuster le seuil d’arrêt de la tail duplication
- Sur clang-19, définir cette limite à une valeur très élevée permet de retrouver un comportement similaire
- Dans les builds LTO, la transmission de cette option est complexe
- La tail duplication a lieu pendant la génération de code, et dans une build LTO, la génération de code intervient au link time, pas au compile time
- Il faut donc passer le flag non seulement au compilateur, mais aussi à
lld
- L’exemple de configuration utilisé consiste à passer
-mllvm -tail-dup-pred-size=5000àOPTetLDFLAGSlors de l’étape./configure
Le problème de base de référence révélé par les benchmarks
- Les benchmarks peuvent mesurer précisément les différences de performance entre certaines builds, mais étendre ces résultats à une « amélioration générale des performances » exige des hypothèses supplémentaires
- Les benchmarks de l’interpréteur à appels terminaux ont montré des résultats 10 à 15 % plus rapides que l’ancien interpréteur computed goto, mais la base de référence était trop complexe pour généraliser vers une conclusion plus large
- Dans le travail de performance, le choix de la base de comparaison est un problème récurrent
- Même si l’on comprend théoriquement la meilleure approche connue du moment, régler correctement l’OS, les options du compilateur et les flags en pratique est un autre problème
- Des benchmarks publics réalisés sur du matériel ancien ou à une échelle difficile à reproduire peuvent ne pas convenir à une comparaison directe
- Dans les articles de machine learning aussi, lorsqu’une amélioration d’algorithme est revendiquée, la question importante est souvent d’abord « à quelle base de référence cela a-t-il été comparé ? », plus que « qu’est-ce qui a été fait ? »
- Des résultats impressionnants peuvent facilement apparaître lorsque la comparaison se fait avec une base de référence mal réglée
Compilateurs optimisants et musttail
- Le cas de computed goto montre que les attentes envers les compilateurs optimisants peuvent entrer en conflit
- Le compilateur doit respecter l’intention du programmeur et préserver le même comportement
- En même temps, il doit aussi effectuer des transformations complexes et peu intuitives pour rendre le code plus rapide
- clang-19 a compilé correctement l’interpréteur computed goto du point de vue du comportement du programme, mais a produit une sortie totalement différente de l’intention d’optimisation
- D’autres versions de Clang appliquent l’optimisation voulue même à un interpréteur simple basé sur
switch() - Le computed goto au niveau du code source et la réplication du dispatch au niveau du code machine apparaissent presque comme des concepts orthogonaux
- Comme le résultat d’exécution est identique, les outils actuels peinent à exprimer cette différence de façon cohérente
- L’interpréteur à appels terminaux repose sur l’attribut musttail
musttailne change pas le comportement traditionnellement observable du programme, mais ressemble davantage à une conversation avec l’optimiseur- Le compilateur doit être capable d’effectuer une optimisation donnée, et la compilation doit échouer si cette optimisation n’a pas lieu
- Ce style pourrait permettre d’écrire du code sensible aux performances de façon plus robuste au fil de l’évolution des compilateurs
- Il serait aussi intéressant d’étudier si un attribut hypothétique comme
[[clang::musttailduplicate]]pourrait remplacer le computed goto de la bouclewhilede l’interpréteur
Reproductibilité et limites offertes par nix
nixa beaucoup aidé à gérer plusieurs builds d’interpréteurs Python- Pendant les expérimentations, des dizaines d’interpréteurs Python ont été compilés et benchmarkés avec quatre compilateurs (
gcc,clang-18,clang-19,clang-20) et diverses combinaisons de flags
- Pendant les expérimentations, des dizaines d’interpréteurs Python ont été compilés et benchmarkés avec quatre compilateurs (
- L’utilisation de
nixa permis de conserver des versions parallèles de façon reproductible et isolée- Il était possible de savoir avec certitude de quels compilateur et flags provenait chaque build
- La définition de la matrice de builds pouvait aussi être gérée avec une courte abstraction
- Compiler un LLVM personnalisé avec le patch de correction du bug, puis compiler Python avec ce compilateur, était également possible en une dizaine de lignes de code
- Des inconvénients subsistent
nixdiffère des modes habituels d’utilisation des logiciels, et il est difficile d’exclure totalement que ces différences aient influencé les benchmarks ou les conclusions- Par exemple, nix compile par défaut les projets avec certains flags de hardening, et il a été découvert tôt que ces flags avaient un impact disproportionné sur l’interpréteur à appels terminaux
- Nix est puissant en matière de scalabilité et de personnalisation, mais trouver une méthode précise de personnalisation a demandé beaucoup d’essais et d’exploration du code source de
nixpkgs
1 commentaires
Avis de Hacker News
Je suis l’auteur de la PR qui a intégré l’interpréteur à appels terminaux dans CPython
Tout d’abord, merci à Nelson d’avoir passé près d’un mois à trouver la cause de ce problème
Ni moi ni, probablement, l’équipe CPython ne nous attendions à ce qu’il y ait un tel bug dans le compilateur utilisé comme référence ; j’ai fait une grosse erreur, et j’en suis très embarrassé et désolé
J’ai aussi publié un billet d’excuses : https://fidget-spinner.github.io/posts/apology-tail-call.htm...
Il avait été annoncé une amélioration des performances de 10 à 15 %, alors qu’avec un compilateur sans bug, on est plutôt autour de 1 à 5 % ; et même ces chiffres n’étaient pas complètement faux, ils n’étaient simplement valables que dans certaines conditions
Vous avez produit une amélioration, vous l’avez mesurée, et la PR a été relue : le travail a donc été fait. Il se trouve que la version de clang utilisée pour les mesures a posé problème et rendu les chiffres trompeurs, mais cela ressemble à une erreur raisonnable dans laquelle n’importe qui aurait pu tomber
Malgré tout, cela a apporté une amélioration de performance significative et permis de trouver une régression du compilateur ; les chiffres erronés paraissent mineurs en comparaison. Je ne vois pas très bien qui a réellement subi un préjudice dans cette affaire, et je ne pense pas que cela nécessitait des excuses
Si un gain de 3 à 5 % se maintient dans un système aussi ancien que l’interpréteur Python, c’est déjà une grande réussite, et il y a largement de quoi en être fier
Après environ 30 ans, j’ai tendance à me méfier de toute amélioration de performance significative dans un système qui existe depuis longtemps, surtout au-delà de 1 %
Il existe bien de vraies améliorations, mais elles sont rares ; souvent, on n’a fait que déplacer le temps ailleurs, dans un endroit que le benchmark ne capture pas. De plus, les benchmarks se font dans des environnements contrôlés pour isoler les effets, alors que les logiciels réels tournent avec toutes sortes d’autres choses, sur des VM ou des postes de travail
J’ai vu beaucoup d’améliorations qui semblaient clairement importantes dans un environnement isolé disparaître, voire devenir négatives, une fois en production
CPython est encore plus difficile, car il doit cibler de nombreux environnements, et il n’existe pas de cible de production unique permettant de dire : “si ce n’est pas plus rapide en production, ce n’est pas vraiment plus rapide”. Améliorer les performances dans ce monde-là est vraiment difficile
Au final, le tuning et la mesure des performances sont très difficiles, et la seule chose dont on pourrait s’excuser, c’est d’avoir appris ce fait
J’espère que vous n’aurez pas peur de vous tromper. De toute façon, tout le monde se trompe. Il suffit de faire comme ici : dire “on dirait que nous avons raté quelque chose”, puis déterminer comment le traiter et comment l’éviter à l’avenir
[1] C’est fréquent non seulement pour les performances, mais aussi dans les processus humains. Par exemple, si l’équipe d’un outil de revue de code dit “nous avons réduit le temps de revue de code de 15 %, accélérant ainsi le flux de travail de tout le monde”, il se peut qu’en réalité elle ait créé davantage de travail ailleurs dans le système, que le flux global ne soit pas devenu plus rapide, et qu’elle ait simplement déplacé ces 15 % vers un endroit non mesuré
En théorie, avec ce type de graphe de flot de contrôle et de profil, le compilateur devrait disposer de suffisamment d’informations pour produire un code optimal pour un interpréteur traditionnel basé sur
switch(). Mais en pratique, quand une fonction est aussi grande et aussi interconnectée, on finit par se battre contre le compilateurIl spill des variables importantes que l’on voudrait garder dans des registres, remonte des manipulations de frame de pile que l’on voudrait réduire autour des appels de fonctions de fallback, et fusionne des chemins de code identiques que l’on voulait séparer pour des raisons de prédiction de branchement. Cela peut donner l’impression de jouer du piano avec des gants
Ici aussi, c’est précisément cette “fusion de chemins de code identiques” qui s’est produite, et le compilateur “bugué” a fusionné ces chemins identiques, dégradant les performances
Le compilateur “corrigé” ne le fait plus, mais cette correction revient finalement surtout à ajuster des heuristiques internes du compilateur. Rien ne garantit que ce compilateur, ou d’autres, conserveront à l’avenir des heuristiques qui nous soient favorables
À l’inverse, un interpréteur à appels terminaux permet d’exprimer dans l’interpréteur lui-même le motif de code machine souhaité. En combinant les attributs
musttail,noinlineetpreserve_none, on peut contraindre le problème de façon à dépendre beaucoup moins des heuristiques de l’optimiseurL’intérêt de l’interpréteur à appels terminaux dépasse donc un simple gain de performances de 3 à 5 %, et, avec certains compilateurs, il peut s’agir d’une amélioration fiable des performances encore plus importante
Peut-on améliorer les benchmarks pour éviter des situations similaires ?
[0] https://github.com/faster-cpython/benchmarking-public
Le benchmarking est vraiment terriblement difficile à faire correctement. Il y a trop d’éléments qui peuvent tromper
Récemment, je pensais avoir trouvé une manière de rendre un algorithme environ 15 % plus rapide. En tout cas, tous les benchmarks le disaient
Mais même après avoir dupliqué la fonction plus rapide dans le harnais de test sans jamais l’appeler, en n’appelant en réalité que l’ancienne version plus lente, c’était toujours 15 % plus rapide. Du code qui ne s’exécutait même pas rendait donc le code d’origine plus rapide
Évidemment, c’était une question de disposition du code et de la mémoire : quelque chose avait bougé et tombait mieux dans le cache CPU
Il est vraiment difficile de savoir si le gain de vitesse obtenu vient du fait que le code est réellement « meilleur », ou si l’on a simplement eu de la chance avec un meilleur alignement quelque part
Casey Muratori écrit sur Substack une série très intéressante à ce sujet
Félicitations à l’auteur de l’article pour avoir creusé et mis au jour ce qui se passait vraiment. L’interpréteur à appels terminaux de Python 3.14 reste une bonne amélioration, et quelques pourcents de gain dans un runtime de langage sont des résultats difficiles à obtenir
Mais ce n’était pas le déjeuner gratuit magique à 15 %
Plus important encore, cette affaire illustre bien l’importance de la rigueur dans les benchmarks et des tests dans plusieurs environnements. Elle a aussi révélé un bug de compilateur qui pourrait profiter à tout le monde
C’est le genre d’analyse approfondie qui pousse à revérifier la prochaine grande affirmation de gain de performances. La question à se poser ensuite est : parmi les nombreux résultats « X % plus rapide » actuellement publiés, combien sont en réalité des artefacts de benchmark ou des régressions inconnues ?
Comment mieux éviter ces pièges à l’avenir ?
Le compilateur lui-même n’est-il pas benchmarké ? Les benchmarks existants côté compilateur ou côté Python n’utilisaient-ils pas ce compilateur ?
C’est un bon exemple montrant à quel point il est inexact de dire que C est « proche de la machine » ou un « assembleur portable ». Les optimiseurs modernes réécrivent hardiment la logique tant qu’il n’y a pas d’effet observable
L’article dit aussi que « clang-19 compile l’interpréteur en computed goto “correctement”, au sens où le binaire produit donne toutes les valeurs attendues, mais en même temps cette sortie va complètement à l’encontre de l’intention d’optimisation. En outre, d’autres versions de compilateur appliquent à l’interpréteur “naïf” basé sur
switch()l’optimisation exacte que nous voulions obtenir en réécrivant le code source »En C, on peut croire que
a += 1incrémente une valeur numérique, alors qu’en C++ la même expression peut allouer de la mémoire, dérouler la pile d’appels ou faire on ne sait quoi. De même,a = "a"est en C une simple affectation de pointeur, tandis qu’en C++ cela peut impliquer une allocation mémoire, etc.Dire que « C est un assembleur portable » ne signifie pas que chaque instruction est compilée directement en code machine équivalent
Il n’est pas surprenant que le compilateur, en touchant à la structure de la boucle, ait rendu l’ensemble de l’interpréteur à appels terminaux moins efficace qu’annoncé
Les implémentations d’interpréteurs paranoïaques reviennent parfois à de l’assembleur écrit directement. LuaJIT est célèbre pour avoir implémenté un système de macros afin de rendre une boucle assembleur très efficace portable entre architectures. C’est aussi pour cela qu’il est amusant de toucher à ce genre de choses
Il y a quelques années, j’avais aussi écrit un article avec des tests sur les méthodes populaires d’implémentation de boucles d’interpréteur :
https://github.com/vkazanov/bytecode-interpreters-post
Les prédicteurs de branchement modernes peuvent prédire presque parfaitement un unique saut indirect, à condition que la phase d’exécution soit suffisamment longue et que le comportement du code interprété lui-même soit stable
Un article a étudié cela à la fois sur du matériel réel et avec certains prédicteurs de branchement simulés : https://inria.hal.science/hal-01100647/document
Les expériences que j’ai menées dans ce projet vont aussi, de façon anecdotique, dans le même sens. Je ne l’ai pas inclus dans l’article, mais j’ai observé plusieurs interpréteurs avec les compteurs CPU matériels et
perf stat, et les mauvaises prédictions de branchement n’apparaissaient pas comme le facteur dominantÉvaluer les performances des builds Python est extrêmement difficile, car il existe trop de techniques de build susceptibles d’améliorer les performances
Récemment, les gens d’astral ont aussi rencontré ce problème en montrant que les builds conda-forge étaient nettement plus rapides que la plupart des autres :
https://github.com/astral-sh/python-build-standalone/pull/54...
Je me demande comment l’interpréteur à appels terminaux se comporte lorsqu’il est utilisé avec d’autres optimisations de build existantes
L’auteur y essaie, avec un algorithme génétique, plusieurs combinaisons de compilateurs et de flags d’optimisation
Discussions connexes :
https://docs.python.org/3.14/whatsnew/3.14.html#whatsnew314-... --> https://news.ycombinator.com/item?id=42999672 (66 points | il y a 25 jours | 22 commentaires)
https://blog.reverberate.org/2025/02/10/tail-call-updates.ht... --> https://news.ycombinator.com/item?id=43076088 (124 points | il y a 18 jours | 92 commentaires)
Bon article. Un détail m’a frappé
Dans l’un des articles cités, https://simonwillison.net/2025/Feb/13/python-3140a5/, il écrit que « 3.14.0a5 était 1,12 fois plus rapide que 3.13 dans le benchmark, sur mon M2 MacBook Pro extrêmement surchargé »
Cette partie prête pas mal à confusion. Est-ce que cela veut dire qu’il a lancé le benchmark alors que l’ordinateur était surchargé par d’autres processus ? Dans ce cas, les résultats ne sont-ils pas totalement peu fiables ?
Je pensais que ce genre de benchmark était réalisé dans un environnement très contrôlé afin d’éliminer les variables externes
Pour CPython, voir https://github.com/faster-cpython/benchmarking-public
Certains ici qualifient 10 % de « beaucoup » et 1 % de « normal », mais une optimisation comme l’inlining partiel du Fibonacci doublement récursif peut réduire exponentiellement la charge de travail réelle et le temps
Avec des arguments à deux chiffres, on peut dépasser 10×, c’est-à-dire des milliers de pour cent. Strictement parlant, c’est exponentiel par rapport à la différence de profondeur de récursion, pas à la taille du problème [1]
Les compilateurs C peuvent aussi être très sensibles aux heuristiques d’inlining du code, si bien que l’apparition effective de cet énorme gain de vitesse peut dépendre fortement de la forme du code
Une partie du problème vient donc du fait que les CPU sont devenus extrêmement sophistiqués et complexes, mais un autre aspect est que les compilateurs au-delà de
-O0ou-O1sont eux aussi devenus sophistiqués et complexesCet article est bon et mérite d’être lu, mais c’est aussi l’un des innombrables exemples où l’interaction de deux choses complexes peut produire des résultats très surprenants. C’est vrai aussi en dehors de l’informatique
Les gens ont fortement tendance à simplifier à l’excès, même quand cette leçon se répète encore et encore
En plus, l’article utilise au moins deux CPU, Intel et Apple M1, et deux compilateurs, gcc et clang, mais les environnements de déploiement réels peuvent inclure bien davantage de générations et d’implémentations Intel, AMD et ARM, ainsi que d’autres compilateurs. Cela ne fait qu’échantillonner une toute petite partie de la complexité totale
Pour faire les choses plus scientifiquement, en particulier pour des écarts comme « 1,01× », il faudrait des barres d’erreur d’une forme ou d’une autre sur les mesures de temps. L’écart type de la moyenne, ou dans ce cas peut-être l’écart type du minimum, serait plus approprié [2]
Pour réduire l’erreur de mesure, il faudra probablement aussi que l’OS planifie l’exécution en fixant les cœurs CPU
[1] https://stackoverflow.com/questions/360748/computational-com...
[2] https://github.com/c-blake/bu/blob/main/doc/tim.md
J’ai récemment benchmarké Python 3.9 à 3.13, et les choses se sont améliorées continuellement jusqu’à 3.11
Mais Python 3.12 et 3.13 étaient environ 10 % plus lents que 3.11
Je pensais que mon benchmark maison n’était pas assez bon, mais je l’ai quand même déployé sur un service critique, et les métriques collectées montraient le même changement
Quelqu’un d’autre a-t-il rencontré le même problème ?
[0]: https://github.com/python/cpython/issues/123540