
Python 3.14 et son nouveau compilateur JIT
marque un point important dans l’évolution du langage de programmation le plus populaire au monde. Bien que Python soit reconnu depuis longtemps pour sa lisibilité et son vaste écosystème, sa vitesse d’exécution a souvent été « l’éléphant dans la pièce ».
Avec l’arrivée de la version 3.14, l’équipe de développement principale de CPython a livré non pas une, mais deux des fonctionnalités les plus attendues ces derniers temps.
La fin du GIL
J’ai déjà écrit à ce sujet auparavant. La véritable concurrence est désormais disponible en Python si vous le souhaitez. Si vous souhaitez plus de détails sur Python sans GIL, je laisserai un lien vers mon article à ce sujet à la fin.
Le compilateur Just-In-Time (JIT)
Cette fonctionnalité expérimentale est désormais intégrée directement dans les installateurs officiels, et c’est sur cela que nous allons nous concentrer ici. C’est le résultat d’années de préparation architecturale effectuée par l’équipe principale de Python et d’autres, visant à rendre Python « plus rapide par défaut » sans briser l’écosystème d’extension C qui alimente tout, de la science des données aux backends Web.
Dans cet article, nous allons soulever le capot du nouveau JIT, explorer comment il se différencie des efforts d’optimisation précédents et passer en revue une méthodologie d’analyse comparative pour vous aider à décider s’il est temps d’essayer le JIT sur vos charges de travail.
Qu’est-ce que le nouveau compilateur Just-In-Time (JIT) de Python ?
Pour comprendre le JIT 3.14, nous devons comprendre comment Python s’exécute traditionnellement. Python standard (CPython) est un interprété langue. Lorsque vous exécutez un script, votre code est compilé dans code d’octet, qui est un ensemble d’instructions que la machine virtuelle CPython exécute.
Le JIT modifie ce flux. Au lieu de simplement interpréter le bytecode ligne par ligne, le JIT surveille quelles parties de votre code sont exécutées le plus fréquemment (les chemins « chauds »). Lorsqu’une fonction ou une boucle est jugée « chaude », le JIT traduit le bytecode en code machine natif (instructions comprises par le processeur). Ensuite, la prochaine fois que le code sera invoqué, aucune interprétation n’est requise. Au lieu de cela, il fonctionne tel quel. Cela peut vous faire gagner beaucoup de temps, comme nous le verrons plus tard.
Comment le JIT s’intègre dans CPython
Le Python 3.14 JIT n’est pas une réécriture totale. Il est conçu comme un composant opt-in qui fonctionne parallèlement à l’interprète existant. Il utilise une technique appelée « copie et patch », qui permet au JIT d’être léger et portable sur différentes architectures de processeur sans nécessiter un backend de compilateur massif et complexe comme LLVM.
Qu’est-ce qui a changé dans Python 3.14 ?
Python 3.13 avait un JIT expérimental de base, mais il était désactivé par défaut. Si vous vouliez le tester, vous deviez cloner l’arborescence des sources CPython et la compiler avec des indicateurs expérimentaux spécifiques tels que - - enable-experimental-jit.
Avec Python 3.14, tout a changé. Il proposait le JIT dans les installateurs officiels .msi (Windows) et .pkg (macOS). Cela signifiait également que vous n’aviez plus besoin d’un compilateur C sur votre machine pour bénéficier des avantages du JIT. Bien qu’elle soit encore « expérimentale », l’inclusion dans les binaires officiels indique que l’équipe principale estime que le JIT est suffisamment stable pour des tests à grande échelle par la communauté.
Obtenir Python 3.14
Rendez-vous sur https://www.python.org/downloads/et vous verrez une option de téléchargement pour la version 3.14. Cliquez dessus, puis suivez les instructions.
Alternativement, si vous disposez du UV outil installé, vous pouvez taper ce qui suit.
PS C:\ > uv python install 3.14
Activation du JIT
Par défaut, le JIT est désactivé. Il s’agit d’une mesure de sécurité ; parce qu’il est expérimental, le Python Steering Council veut s’assurer que les utilisateurs ne soient pas confrontés à des régressions inattendues de la stabilité ou de l’utilisation de la mémoire sans le choisir explicitement.
Pour activer le JIT, vous utilisez une variable d’environnement. Cela indique au runtime CPython d’initialiser le moteur JIT au démarrage.
Sous Windows (PowerShell) :
$env:PYTHON_JIT=1
python my_script.py
Sur macOS/Linux (Bash/Zsh) :
PYTHON_JIT=1
python my_script.py
Une fois activé, CPython ne compile pas tout JIT immédiatement. Il utilise un système de hiérarchisation. Fondamentalement, il essaie d’abord d’exécuter le code le moins cher possible et consacre uniquement des efforts de compilation/optimisation aux parties qui s’avèrent chaudes.
- Niveau 0 : Interprétation standard.
- Niveau 1 : Bytecode spécialisé (introduit dans la version 3.11).
- Niveau 2 (Le JIT) : Génération de code machine pour les chemins les plus fréquemment utilisés.
Mesurer l’impact du JIT
Lorsque vous testez un JIT, vous ne pouvez pas simplement utiliser le temps.temps() autour d’une fonction. Les JIT nécessitent un période d’échauffement. Les premières itérations d’une boucle peuvent être plus lentes que la normale à mesure que le JIT profile le code, mais les itérations suivantes peuvent être nettement plus rapides.
La suite de référence
Vous trouverez ci-dessous une suite de tests complète conçue pour mettre en pratique différents aspects du JIT, des mathématiques lourdes à la manipulation d’objets complexes.
Fichier 1 : workloads.py
Ce fichier contient trois tâches différentes liées au processeur.
1/ La fonction Mandelbrot itère la formule de Mandelbrot sur une grille de pixels et renvoie une somme de contrôle du nombre d’itérations par pixel.
2/ La fonction Djikstra construit un graphe déterministe pondéré aléatoirement et exécute Dijkstra à partir du nœud 0, renvoyant le nombre de nœuds finalisés/visités.
3/ La fonction Levenshtein génère N paires de chaînes aléatoires déterministes et renvoie la somme de leurs distances Levenshtein
from __future__ import annotations
import random
import heapq
# Workload 1: Mandelbrot (CPU + math loops)
def mandelbrot(width: int = 1000, height: int = 1000, iters: int = 500) -> int:
checksum = 0
for y in range(height):
cy = (y / height) * 2.4 - 1.2
for x in range(width):
cx = (x / width) * 3.2 - 2.2
zx, zy, count = 0.0, 0.0, 0
while zx * zx + zy * zy <= 4.0 and count < iters:
zx, zy = zx * zx - zy * zy + cx, 2.0 * zx * zy + cy
count += 1
checksum += count
return checksum
# Workload 2: Dijkstra (heap + list + logic)
def dijkstra(n: int = 10000, edges_per_node: int = 50, seed: int = 123) -> int:
rng = random.Random(seed)
graph = [[] for _ in range(n)]
for u in range(n):
for _ in range(edges_per_node):
v = rng.randrange(n)
if v != u:
graph[u].append((v, rng.randrange(1, 30)))
dist = [10**12] * n
dist[0] = 0
pq = [(0, 0)]
visited = 0
while pq:
d, u = heapq.heappop(pq)
if d != dist[u]:
continue
visited += 1
for v, w in graph[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
heapq.heappush(pq, (nd, v))
return visited
# Workload 3: Levenshtein distance (dynamic programming)
def levenshtein(a: str, b: str) -> int:
prev = list(range(len(b) + 1))
for i, ca in enumerate(a, 1):
cur = [i]
for j, cb in enumerate(b, 1):
cur.append(min(cur[j - 1] + 1, prev[j] + 1, prev[j - 1] + (ca != cb)))
prev = cur
return prev[-1]
def levenshtein_batch(n: int = 10000, seed: int = 7, k: int = 50) -> int:
"""
Deterministic batch: fixed RNG seed, fixed alphabet, fixed string length.
Returns the sum of distances.
"""
rng = random.Random(seed)
alphabet = "abc"
total = 0
for _ in range(n):
a = "".join(rng.choices(alphabet, k=k))
b = "".join(rng.choices(alphabet, k=k))
total += levenshtein(a, b)
return total
Fichier 2 : benchmark.py
Ce script automatise la comparaison de différentes charges de travail avec JIT activé et désactivé.
import os
import time
import json
import subprocess
from pathlib import Path
PYTHON_EXE = r"C:\Users\thoma\AppData\Local\Programs\Python\Python314\python.exe"
PROJECT_DIR = Path(__file__).resolve().parent
# Original workloads (statement prints a result for sanity)
WORKLOADS = [
("mandelbrot", 'from workloads import mandelbrot; print(mandelbrot())'),
("dijkstra", 'from workloads import dijkstra; print(dijkstra())'),
("levenshtein_batch", 'from workloads import levenshtein_batch; print(levenshtein_batch())'),
]
N_RUNS = 10 # average of ALL runs (set to 6/10/20 as you like)
OUTFILE = PROJECT_DIR / "results_avg.json"
def run_once(stmt: str, jit_val: int) -> tuple[float, str]:
env = os.environ.copy()
env["PYTHON_JIT"] = str(jit_val)
# Ensure local workloads.py is importable in subprocess
env["PYTHONPATH"] = str(PROJECT_DIR) + (os.pathsep + env.get("PYTHONPATH", ""))
t0 = time.perf_counter()
p = subprocess.run(
[PYTHON_EXE, "-c", stmt],
env=env,
cwd=str(PROJECT_DIR),
capture_output=True,
text=True,
)
t1 = time.perf_counter()
if p.returncode != 0:
raise RuntimeError(
f"Run failed (PYTHON_JIT={jit_val})\n\n"
f"Statement:\n{stmt}\n\n"
f"STDOUT:\n{p.stdout}\n\nSTDERR:\n{p.stderr}"
)
return (t1 - t0, p.stdout.strip())
def summarize(times: list[float]) -> dict:
return {
"avg": sum(times) / len(times),
"min": min(times),
"max": max(times),
"runs": times,
}
def bench_workload(name: str, stmt: str) -> dict:
results = {}
outputs = {}
for jit_val in (0, 1):
times = []
outs = []
print(f" PYTHON_JIT={jit_val}: running {N_RUNS} times...")
for i in range(1, N_RUNS + 1):
dt, out = run_once(stmt, jit_val)
times.append(dt)
outs.append(out)
print(f" run {i}/{N_RUNS}: {dt:.6f}s")
results[jit_val] = summarize(times)
outputs[jit_val] = outs
avg0 = results[0]["avg"]
avg1 = results[1]["avg"]
speedup = avg0 / avg1 if avg1 else float("inf")
delta_pct = (avg1 - avg0) / avg0 * 100.0 if avg0 else 0.0
return {
"workload": name,
"jit0": results[0],
"jit1": results[1],
"speedup_jit0_over_jit1": speedup,
"delta_pct_jit1_vs_jit0": delta_pct,
"outputs": outputs, # sanity: should be stable
}
def main() -> int:
all_results = []
print(f"Using Python: {PYTHON_EXE}")
print(f"Project dir: {PROJECT_DIR}")
print(f"Runs per setting (avg of all runs): {N_RUNS}\n")
for name, stmt in WORKLOADS:
print(f"=== {name} ===")
r = bench_workload(name, stmt)
all_results.append(r)
print(f"\n Averages:")
print(f" JIT=0 avg: {r['jit0']['avg']:.6f}s (min {r['jit0']['min']:.6f}, max {r['jit0']['max']:.6f})")
print(f" JIT=1 avg: {r['jit1']['avg']:.6f}s (min {r['jit1']['min']:.6f}, max {r['jit1']['max']:.6f})")
print(f" Speedup (JIT=0 / JIT=1): {r['speedup_jit0_over_jit1']:.3f}× (Δ={r['delta_pct_jit1_vs_jit0']:+.2f}%)\n")
# Optional: warn if outputs vary across runs (nondeterminism)
if len(set(r["outputs"][0])) != 1:
print(" !! WARNING: JIT=0 output differs across runs (nondeterministic workload?)")
if len(set(r["outputs"][1])) != 1:
print(" !! WARNING: JIT=1 output differs across runs (nondeterministic workload?)")
OUTFILE.write_text(json.dumps(all_results, indent=2), encoding="utf-8")
print(f"Wrote: {OUTFILE}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Voici mes résultats.
C:\Users\thoma\projects\python_jit>C:\Users\thoma\AppData\Local\Programs\Python\Python314\python.exe benchmark.py
Using Python: C:\Users\thoma\AppData\Local\Programs\Python\Python314\python.exe
Project dir: C:\Users\thoma\projects\python_jit
Runs per setting (avg of all runs): 10
=== mandelbrot ===
PYTHON_JIT=0: running 10 times...
run 1/10: 6.890924s
run 2/10: 6.950737s
run 3/10: 7.265357s
run 4/10: 6.947150s
run 5/10: 6.932333s
run 6/10: 6.939378s
run 7/10: 7.194705s
run 8/10: 6.995550s
run 9/10: 6.902696s
run 10/10: 7.256164s
PYTHON_JIT=1: running 10 times...
run 1/10: 5.216740s
run 2/10: 5.241888s
run 3/10: 5.350822s
run 4/10: 5.246767s
run 5/10: 5.294771s
run 6/10: 5.273295s
run 7/10: 5.272135s
run 8/10: 5.617062s
run 9/10: 5.251656s
run 10/10: 5.239060s
Averages:
JIT=0 avg: 7.027499s (min 6.890924, max 7.265357)
JIT=1 avg: 5.300420s (min 5.216740, max 5.617062)
Speedup (JIT=0 / JIT=1): 1.326× (Δ=-24.58%)
=== dijkstra ===
PYTHON_JIT=0: running 10 times...
run 1/10: 0.235401s
run 2/10: 0.227603s
run 3/10: 0.244492s
run 4/10: 0.232971s
run 5/10: 0.249589s
run 6/10: 0.232229s
run 7/10: 0.229422s
run 8/10: 0.238399s
run 9/10: 0.230657s
run 10/10: 0.235772s
PYTHON_JIT=1: running 10 times...
run 1/10: 0.238862s
run 2/10: 0.239266s
run 3/10: 0.240312s
run 4/10: 0.231413s
run 5/10: 0.232692s
run 6/10: 0.233783s
run 7/10: 0.230016s
run 8/10: 0.237760s
run 9/10: 0.240895s
run 10/10: 0.246033s
Averages:
JIT=0 avg: 0.235653s (min 0.227603, max 0.249589)
JIT=1 avg: 0.237103s (min 0.230016, max 0.246033)
Speedup (JIT=0 / JIT=1): 0.994× (Δ=+0.62%)
=== levenshtein_batch ===
PYTHON_JIT=0: running 10 times...
run 1/10: 2.176256s
run 2/10: 2.171253s
run 3/10: 2.171834s
run 4/10: 2.170444s
run 5/10: 2.149874s
run 6/10: 2.162820s
run 7/10: 2.171975s
run 8/10: 2.199151s
run 9/10: 2.168398s
run 10/10: 2.167821s
PYTHON_JIT=1: running 10 times...
run 1/10: 1.575666s
run 2/10: 1.612615s
run 3/10: 1.571106s
run 4/10: 1.584650s
run 5/10: 1.579948s
run 6/10: 1.582633s
run 7/10: 1.593924s
run 8/10: 1.573608s
run 9/10: 1.581427s
run 10/10: 1.578553s
Averages:
JIT=0 avg: 2.170983s (min 2.149874, max 2.199151)
JIT=1 avg: 1.583413s (min 1.571106, max 1.612615)
Speedup (JIT=0 / JIT=1): 1.371× (Δ=-27.06%)
Interprétation des résultats
Comme vous pouvez le constater, les résultats sont mitigés. C’est normal pour un JIT expérimental.
- 10 à 30 % d’accélération : Courant dans les boucles « Python pur » (comme les tests de Mandelbrot ou de Levenshtein) où le JIT peut éviter la surcharge de la boucle de répartition du bytecode.
- 0 % d’amélioration : Courant dans les tâches liées aux E/S ou dans le code qui utilise fortement les extensions C. Le code Dijkstra n’a pas été accéléré car son temps d’exécution est dominé par des opérations de tas/tuple et un travail gourmand en mémoire et basé sur l’allocation que le CPython JIT actuel n’optimise pas de manière significative, de sorte que les économies d’interpréteur sont perdues dans le bruit.
Quand utiliser Python 3.14 JIT
Le JIT est un outil puissant, mais ce n’est pas un « bouton magique ». D’après mon expérience, vous devriez essayer le JIT lorsque vous avez…
- Logique liée au processeur : Votre application effectue des calculs lourds, du traitement de données ou une logique complexe en Python pur.
- Processus de longue durée : Des serveurs Web (Gunicorn/Uvicorn) ou des travailleurs en arrière-plan (Celery) qui fonctionnent pendant des heures, ce qui laisse au JIT suffisamment de temps pour s’échauffer et optimiser les chemins chauds.
- Tests expérimentaux : Vous souhaitez préparer votre base de code pour les futures versions de Python (3.15+), où le JIT sera probablement plus agressif.
Et évitez-le quand vous en avez…
- Applications liées aux E/S: Si votre application attend simplement des requêtes de base de données ou des réponses d’API, le JIT ne vous aidera pas.
- Environnements limités en mémoire : Les petites fonctions Lambda ou les minuscules conteneurs peuvent souffrir de l’empreinte mémoire accrue du cache JIT.
- Outils CLI de courte durée : Un script qui s’exécute en moins d’une seconde n’a pas besoin de JIT.
Orientations futures : au-delà de la version 3.14
L’équipe principale de CPython considère la 3.14 comme « l’année de fondation ». Les futures itérations (Python 3.15 et 3.16) devraient inclure :
- Passes d’optimisation plus approfondies : Utiliser les informations de type collectées lors de l’exécution pour effectuer une génération de code machine encore plus agressive.
- Meilleures heuristiques : Des décisions plus intelligentes sur quand à compiler, réduisant ainsi la pénalité « d’échauffement ».
- Frais généraux réduits : Affiner le mécanisme de copie et de correctif pour réduire la consommation de mémoire.
Résumé
Le JIT de Python 3.14 est plus qu’un simple correctif de performances. C’est une déclaration d’intention. Cela montre que Python souhaite sérieusement combler l’écart de performances avec des langages comme Java ou Go tout en conservant la simplicité « batteries incluses » qui l’a rendu célèbre.
Pour la plupart des développeurs, JIT est simplement un autre outil à surveiller. Si les performances sont importantes dans vos projets, cela vaut la peine de tester Python 3.14 par rapport à vos charges de travail existantes. Quelques tests sur vos chemins de code les plus importants peuvent révéler des gains de performances là où vous ne les attendiez pas.
Voici le lien vers mon précédent article sur GIL Fee Python, que j’ai mentionné au début.



