Le JIT de CPython 3.15 repart de l’avant
(blog.python.org)Résultats clés
| Plateforme | Gain de performance du JIT (vs interpréteur à appels terminaux) |
|---|---|
| macOS AArch64 | +11~12 % |
| x86_64 Linux | +5~6 % |
Portée des benchmarks : très variable, de cas 20 % plus lents à des cas plus de 100 % plus rapides (
unpack_sequencemicrobenchmark exclu)
- Objectif atteint : l’objectif de la 3.15 (+5 %) a été atteint avec plus d’un an d’avance
- Support du free-threading : encore incomplet, travail en cours avec la 3.15/3.16 en ligne de mire
Principaux enseignements
-
Perte d’un sponsor = crise → bascule vers une dynamique communautaire
Même après le retrait en 2025 du sponsor principal de l’équipe Faster CPython, les contributions bénévoles de la communauté ont au contraire fait augmenter le nombre de contributeurs et produit des résultats. -
Découper les problèmes complexes en petites unités
Même sur un projet à forte barrière d’entrée comme le JIT, la décomposition en tâches unitaires et des guides clairs ont permis à des programmeurs C non spécialistes d’y contribuer. -
La réduction du bus factor est un indicateur de bonne santé du projet
Le nombre de contributeurs sur l’étape intermédiaire (optimizer) est passé de 2 à 4, et des développeurs non core sont eux aussi devenus des contributeurs clés. -
L’infrastructure de mesure change le rythme du développement
Un système qui publie chaque jour les performances du JIT (doesjitgobrrr.com) a été déterminant à la fois pour détecter tôt les régressions et pour entretenir la motivation. -
Les échanges entre communautés renforcent le niveau technique
Les échanges avec l’équipe PyPy et des discussions informelles avec des développeurs de compilateurs ont débouché sur de véritables avancées techniques.
Contexte et résultats
[IMG] Graphique des performances du JIT (au 17 mars 2026, plus bas = plus rapide que l’interpréteur)
(Performances du JIT au 17 mars 2026. Plus la valeur est basse, plus il est rapide par rapport à l’interpréteur. Source de l’image : doesjitgobrrr.com)
Bonne nouvelle. Sur macOS AArch64, l’objectif de performance du JIT de CPython, pourtant très modeste, a été atteint avec plus d’un an d’avance ; sur x86_64 Linux, il l’a été avec quelques mois d’avance. Le JIT alpha de la 3.15 est environ 11~12 % plus rapide que l’interpréteur à appels terminaux sur macOS AArch64, et 5~6 % plus rapide que l’interpréteur standard sur x86_64 Linux. Ces chiffres sont des moyennes géométriques et restent préliminaires. En pratique, l’éventail va de cas 20 % plus lents à des cas plus de 100 % plus rapides (unpack_sequence microbenchmark exclu). Il n’existe pas encore de véritable support du free-threading, mais c’est un objectif visé pour la 3.15/3.16. Le JIT est désormais de nouveau sur les rails.
Il est difficile d’exagérer à quel point cela a été ardu. À un moment, il y avait de vraies raisons de douter que le projet JIT puisse aboutir à un gain de vitesse significatif. Avec le recul, le JIT initial de CPython n’apportait en réalité pratiquement aucun gain. Il y a huit mois, j’ai publié un billet rétrospectif sur le JIT expliquant que le JIT originel de CPython 3.13 et 3.14 était souvent plus lent que l’interpréteur. C’était aussi le moment où l’équipe Faster CPython perdait le soutien de son principal sponsor. En tant que bénévole, je n’ai pas été touché directement, mais des collègues qui y travaillaient l’ont été, et pendant un temps l’avenir du JIT a semblé incertain.
Alors, qu’est-ce qui a changé par rapport aux 3.13 et 3.14 ? Je ne vais pas raconter une histoire héroïque où notre ingéniosité aurait sauvé le JIT du naufrage. Franchement, je pense qu’une grande partie du succès actuel tient à la chance. Le bon moment, le bon endroit, les bonnes personnes, les bons choix. Je pense sincèrement que sans Savannah Ostrowski, Mark Shannon, Diego Russo, Brandt Bucher et moi, tous contributeurs majeurs du JIT, cela n’aurait pas été possible. Et pour ne pas oublier d’autres contributeurs JIT très actifs, j’en citerai encore quelques-uns : Hai Zhu, Zheaoli, Tomas Roun, Reiden Ong, Donghee Na, et il y en a sûrement d’autres que j’omets.
Je veux parler d’aspects souvent moins mis en avant du JIT : les personnes et un peu de chance. Si vous cherchez les détails techniques, ils sont ici.
Partie 1 : un JIT piloté par la communauté
L’équipe Faster CPython a perdu son sponsor principal en 2025. J’ai immédiatement proposé une idée de gouvernance communautaire. À l’époque, on ne savait pas du tout si cela fonctionnerait. Le projet JIT avait la réputation de ne pas convenir aux nouveaux contributeurs. Historiquement, il exige beaucoup de connaissances préalables.
Lors du core sprint CPython à Cambridge, l’équipe cœur du JIT s’est réunie et a rédigé un plan visant un JIT 5 % plus rapide d’ici la 3.15, puis 10 % plus rapide et compatible free-threading d’ici la 3.16. Un point moins visible, mais essentiel à la santé du projet, concernait la réduction du bus factor. Nous voulions plus de deux mainteneurs actifs pour chacune des trois étapes du JIT : le sélecteur de régions au frontend, l’optimizer au middle-end, et le générateur de code au backend.
Auparavant, le middle-end du JIT ne comptait que deux contributeurs actifs récurrents. Aujourd’hui, il en compte quatre, et deux développeurs non core (Hai Zhu et Reiden) sont devenus des membres compétents et précieux.
Ce qui a bien marché pour attirer des gens relevait de pratiques classiques d’ingénierie logicielle : découper les problèmes complexes en morceaux gérables. Brandt a commencé à le faire dès la 3.14, en ouvrant plusieurs mega-issues qui fragmentaient les optimisations JIT en tâches simples, du type « essayez d’optimiser une seule instruction dans le JIT ». J’ai repris l’idée de Brandt pour la 3.15. Heureusement, je me suis occupé d’un travail plus facile : des tickets visant à transformer les instructions de l’interpréteur en une forme facile à optimiser. Pour encourager les nouveaux contributeurs, j’ai rédigé des consignes très détaillées, directement exploitables. J’ai aussi bien délimité les unités de travail. Cela semble avoir aidé. Onze contributeurs, moi compris, ont travaillé sur ce ticket et ont converti presque tout l’interpréteur vers une forme compatible avec l’optimizer du JIT. L’idée clé a été de transformer le JIT, d’un bloc opaque, en quelque chose auquel des programmeurs C sans expérience du JIT pouvaient contribuer.
D’autres approches ont aussi été efficaces : encourager les gens et célébrer les avancées, grandes comme petites. Chaque PR JIT avait un résultat concret et visible, ce qui a sans doute aidé à donner une direction.
L’effort d’optimisation communautaire a porté ses fruits. Le JIT est passé de 1 % plus rapide à 3~4 % plus rapide sur x86_64 Linux (voir la courbe bleue ci-dessous) :
[IMG] Performances du JIT vs interpréteur pendant la période d’optimisation communautaire
(Source de l’image : doesjitgobrrr.com)
Partie 2 : des choix heureux
Enregistrement de traces (Trace Recording)
Encore une fois, je pense qu’une grande partie de cela tient à la chance. Lors du core sprint CPython à Cambridge, Brandt m’a convaincu de réécrire le frontend du JIT selon une approche de tracing. Au début, l’idée ne me plaisait pas, mais par esprit de contradiction j’ai fini par me dire : réécrivons-le « pour prouver que ça ne marche pas ».
Le prototype initial a fonctionné en trois jours, mais il a fallu un mois pour obtenir un vrai comportement JIT qui passe la suite de tests. Les premiers résultats étaient catastrophiques : environ 6 % plus lent sur x86_64 Linux. Et juste au moment où j’étais prêt à abandonner, un heureux accident s’est produit : j’avais mal compris une suggestion de Mark.
Mark proposait d’ajouter une table de dispatch supplémentaire à l’interpréteur, avec deux tables de dispatch : une pour l’interpréteur normal, une pour le tracing. Mais j’ai mal compris et construit une version encore plus extrême : au lieu d’une version traçante de chaque instruction ordinaire, je n’ai gardé qu’une seule instruction chargée du tracing, et toutes les entrées de la seconde table pointaient vers elle. Cela s’est révélé être un excellent choix. L’approche initiale à deux tables doublait la taille de l’interpréteur, provoquant un grossissement du code et une baisse naturelle de performance. Avec une seule instruction et deux tables, la taille de l’interpréteur n’augmente que de l’équivalent d’une instruction, ce qui permet de conserver un interpréteur de base très rapide. Nous appelons affectueusement ce mécanisme le dual dispatch.
Un chiffre qui montre l’importance du trace recording : la couverture de code du JIT a augmenté de 50 %. Cela signifie que toutes les optimisations futures auraient été, en simplifiant, 50 % moins efficaces sans cela.
Merci à Brandt et à Mark d’avoir permis, presque par hasard, de tomber sur une solution aussi élégante.
Élimination des comptages de références (Reference Count Elimination)
Un autre bon choix, un peu chanceux lui aussi, a été d’essayer l’élimination des comptages de références. À l’origine, c’était un travail de Matt Page sur l’optimizer de bytecode de CPython. Je me suis aperçu que malgré ce travail sur l’optimizer de bytecode, il restait encore une branche dans le code JITé à chaque décrémentation de compteur de référence. Je me suis dit : « et si on supprimait cette branche ? », sans avoir la moindre idée de l’impact réel. En pratique, même une seule branche coûtait assez cher, et comme il y en avait une ou plus par instruction Python, elles finissaient par s’accumuler.
Autre coup de chance : c’était très facile à paralléliser, et c’est devenu un excellent support pédagogique pour expliquer l’interpréteur et le JIT aux contributeurs. Cela a été la principale optimisation utilisée pour distribuer le travail sur le JIT de Python 3.15. Le processus a surtout consisté en refactorings manuels, mais il donnait aux participants l’occasion d’apprendre sans être submergés par les parties les plus complexes du JIT.
Partie 3 : une excellente équipe
Nous avons une formidable équipe d’infrastructure. En réalité, c’est une seule personne. Et plus concrètement encore, notre « équipe » se résume aujourd’hui à quatre machines qui tournent dans le placard de Savannah. Malgré cela, Savannah a accompli pour le JIT l’équivalent du travail d’une équipe d’infrastructure entière. Sans moyen de publier des chiffres de performance, le JIT n’aurait jamais pu progresser aussi vite. Le fait d’avoir chaque jour les résultats d’exécution du JIT a changé la boucle de feedback. Cela a aidé à détecter les régressions et nous a permis de vérifier que nos optimisations fonctionnaient réellement.
Mark est exceptionnel sur le plan technique. Comme Internet lui accorde déjà bien assez d’éloges, je vais m’arrêter là :).
Diego aussi est remarquable. Il s’occupe du JIT sur le matériel ARM et, récemment, il a commencé à travailler pour rendre le JIT plus compatible avec les profileurs. Il est difficile d’exagérer la complexité de ce problème.
Brandt a posé les bases initiales du backend en code machine. Sans cela, les nouveaux contributeurs auraient dû écrire de l’assembleur, ce qui en aurait sans doute découragé beaucoup plus.
Partie 4 : parler avec les gens
Je tiens à souligner la valeur du dialogue et du partage d’idées.
Merci à CF Bolz-Tereick, qui m’a énormément appris sur PyPy. J’ai passé plusieurs mois à explorer le code source de PyPy, et je pense que cela a fait de moi un meilleur développeur JIT dans l’ensemble. CF a aussi toujours été très disponible lorsque j’avais besoin d’aide.
Je participe à un salon informel sur les compilateurs avec Max Bernstein, sans lequel j’aurais probablement perdu ma motivation depuis longtemps. Max est un auteur prolifique et un spécialiste des compilateurs très accessible.
Les idées n’existent pas dans un vide isolé. Je pense qu’avoir côtoyé pendant un moment des développeurs de compilateurs m’a rendu plus compétent pour écrire un JIT. À tout le moins, me plonger dans PyPy m’a ouvert de nouvelles perspectives !
Conclusion
Les personnes comptent. Et avec un peu de chance en plus, JIT go brrr.
Aucun commentaire pour le moment.