markiert einen wichtigen Punkt in der Entwicklung der weltweit beliebtesten Programmiersprache. Während Python seit langem für seine Lesbarkeit und sein großes Ökosystem bekannt ist, struggle seine Ausführungsgeschwindigkeit oft der „Elefant im Raum“.
Mit der Einführung von 3.14 hat das CPython-Kernentwicklungsteam nicht nur eine, sondern zwei der am meisten erwarteten Funktionen der letzten Zeit bereitgestellt.
Das Ende der GIL
Darüber habe ich schon einmal geschrieben. Echte Parallelität ist jetzt in Python verfügbar, wenn Sie es wünschen. Wenn Sie weitere Informationen zu GIL-freiem Python wünschen, hinterlasse ich am Ende einen Hyperlink zu meinem Artikel darüber.
Der Simply-In-Time (JIT)-Compiler
Diese experimentelle Funktion ist jetzt direkt in den offiziellen Installationsprogrammen enthalten und darauf konzentrieren wir uns hier. Es ist das Ergebnis jahrelanger Architekturvorbereitung durch das Python-Kernteam und andere mit dem Ziel, Python „standardmäßig schneller“ zu machen, ohne das C-Erweiterungs-Ökosystem zu beschädigen, das alles von der Datenwissenschaft bis zu Net-Backends antreibt.
In diesem Artikel werfen wir einen Blick auf das neue JIT, untersuchen, wie es sich von früheren Optimierungsbemühungen unterscheidet, und gehen einige Benchmarking-Methodologien durch, um Ihnen bei der Entscheidung zu helfen, ob es an der Zeit ist, das JIT für Ihre Workloads auszuprobieren.
Was ist Pythons neuer Simply-In-Time (JIT)-Compiler?
Um das 3.14 JIT zu verstehen, müssen wir wissen, wie Python traditionell ausgeführt wird. Commonplace-Python (CPython) ist ein interpretiert Sprache. Wenn Sie ein Skript ausführen, wird Ihr Code kompiliert Bytecode, Hierbei handelt es sich um eine Reihe von Anweisungen, die die virtuelle CPython-Maschine ausführt.
Das JIT ändert diesen Ablauf. Anstatt den Bytecode einfach Zeile für Zeile zu interpretieren, überwacht die JIT, welche Teile Ihres Codes am häufigsten ausgeführt werden (die „heißen“ Pfade). Wenn eine Funktion oder Schleife als „heiß“ eingestuft wird, übersetzt die JIT den Bytecode in nativen Maschinencode (Anweisungen, die die CPU versteht). Beim nächsten Aufruf des Codes ist dann keine Interpretation erforderlich. Stattdessen läuft es einfach so, wie es ist. Das kann eine große Zeitersparnis sein, wie wir später sehen werden.
Wie die JIT in CPython passt
Das Python 3.14 JIT ist keine völlige Neufassung. Es ist als Decide-in-Komponente konzipiert, die neben dem vorhandenen Interpreter arbeitet. Es verwendet eine Technik namens „Copy-and-Patch“, die es ermöglicht, dass JIT leichtgewichtig und über verschiedene CPU-Architekturen hinweg portierbar ist, ohne dass ein riesiges, komplexes Compiler-Backend wie LLVM erforderlich ist.
Was hat sich in Python 3.14 geändert?
Python 3.13 verfügte über ein grundlegendes, experimentelles JIT, das jedoch standardmäßig deaktiviert struggle. Wenn Sie es testen wollten, mussten Sie den CPython-Quellbaum klonen und ihn mit bestimmten experimentellen Flags kompilieren, z - - enable-experimental-jit.
Mit Python 3.14 hat sich alles geändert. Das JIT wurde in den offiziellen Installationsprogrammen .msi (Home windows) und .pkg (macOS) angeboten. Dies bedeutete auch, dass Sie keinen C-Compiler mehr auf Ihrem Laptop benötigten, um die JIT-Vorteile nutzen zu können. Obwohl es noch „experimentell“ ist, signalisiert die Aufnahme in die offiziellen Binärdateien, dass das Kernteam glaubt, dass das JIT stabil genug für breite Group-Checks ist.
Holen Sie sich Python 3.14
Gehen Sie rüber zu https://www.python.org/downloads/und Sie sehen eine Obtain-Possibility für 3.14. Klicken Sie darauf und folgen Sie dann den Anweisungen.
Alternativ, wenn Sie das haben UV Wenn das Software installiert ist, können Sie Folgendes eingeben.
PS C: > uv python set up 3.14
Aktivieren des JIT
Standardmäßig ist die JIT deaktiviert. Dies ist eine Sicherheitsmaßnahme; Da es experimentell ist, möchte der Python Steering Council sicherstellen, dass Benutzer nicht mit unerwarteten Rückschritten in der Stabilität oder Speichernutzung konfrontiert werden, ohne sich ausdrücklich dafür zu entscheiden.
Um die JIT zu aktivieren, verwenden Sie eine Umgebungsvariable. Dadurch wird die CPython-Laufzeitumgebung angewiesen, die JIT-Engine beim Begin zu initialisieren.
Unter Home windows (PowerShell):
$env:PYTHON_JIT=1
python my_script.py
Unter macOS/Linux (Bash/Zsh):
PYTHON_JIT=1
python my_script.py
Sobald CPython aktiviert ist, kompiliert CPython nicht sofort alles per JIT. Es verwendet a Stufensystem. Grundsätzlich wird versucht, den Code zunächst so kostengünstig wie möglich auszuführen, und der Kompilierungs-/Optimierungsaufwand wird nur für die Teile aufgewendet, die sich als heiß erweisen.
- Stufe 0: Standardinterpretation.
- Stufe 1: Spezialisierter Bytecode (eingeführt in 3.11).
- Stufe 2 (JIT): Maschinencode-Generierung für die am häufigsten verwendeten Pfade.
Messung der Auswirkungen des JIT
Beim Testen eines JIT können Sie nicht einfach das verwenden time.time() um eine Funktion. JITs erfordern a Aufwärmphase. Die ersten paar Iterationen einer Schleife können langsamer als regular sein, da die JIT den Code profiliert, nachfolgende Iterationen können jedoch deutlich schneller sein.
Die Benchmark-Suite
Nachfolgend finden Sie eine umfassende Testsuite, die darauf ausgelegt ist, verschiedene Aspekte des JIT zu testen, von schwerer Mathematik bis hin zur komplexen Objektmanipulation.
Datei 1: workloads.py
Diese Datei enthält drei verschiedene CPU-gebundene Aufgaben.
1/ Die Mandelbrot-Funktion iteriert die Mandelbrot-Formel über ein Pixelraster und gibt eine Prüfsumme der Iterationszahlen professional Pixel zurück.
2/ Die Djikstra-Funktion erstellt einen deterministischen, zufallsgewichteten Graphen und führt Dijkstra vom Knoten 0 aus aus und gibt zurück, wie viele Knoten abgeschlossen/besucht wurden.
3/ Die Levenshtein-Funktion generiert N deterministische Zufallszeichenfolgenpaare und gibt die Summe ihrer Levenshtein-Abstände zurück
from __future__ import annotations
import random
import heapq
# Workload 1: Mandelbrot (CPU + math loops)
def mandelbrot(width: int = 1000, peak: int = 1000, iters: int = 500) -> int:
checksum = 0
for y in vary(peak):
cy = (y / peak) * 2.4 - 1.2
for x in vary(width):
cx = (x / width) * 3.2 - 2.2
zx, zy, depend = 0.0, 0.0, 0
whereas zx * zx + zy * zy <= 4.0 and depend < iters:
zx, zy = zx * zx - zy * zy + cx, 2.0 * zx * zy + cy
depend += 1
checksum += depend
return checksum
# Workload 2: Dijkstra (heap + listing + logic)
def dijkstra(n: int = 10000, edges_per_node: int = 50, seed: int = 123) -> int:
rng = random.Random(seed)
graph = (() for _ in vary(n))
for u in vary(n):
for _ in vary(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
whereas pq:
d, u = heapq.heappop(pq)
if d != dist(u):
proceed
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 = listing(vary(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, ok: int = 50) -> int:
"""
Deterministic batch: mounted RNG seed, mounted alphabet, mounted string size.
Returns the sum of distances.
"""
rng = random.Random(seed)
alphabet = "abc"
complete = 0
for _ in vary(n):
a = "".be part of(rng.decisions(alphabet, ok=ok))
b = "".be part of(rng.decisions(alphabet, ok=ok))
complete += levenshtein(a, b)
return complete
Datei 2: benchmark.py
Dieses Skript automatisiert den Vergleich verschiedener Workloads mit aktiviertem und deaktiviertem JIT.
import os
import time
import json
import subprocess
from pathlib import Path
PYTHON_EXE = r"C:UsersthomaAppDataLocalProgramsPythonPython314python.exe"
PROJECT_DIR = Path(__file__).resolve().mother or father
# Authentic workloads (assertion prints a consequence 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 # common of ALL runs (set to six/10/20 as you want)
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)
# Guarantee native 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,
textual content=True,
)
t1 = time.perf_counter()
if p.returncode != 0:
elevate RuntimeError(
f"Run failed (PYTHON_JIT={jit_val})nn"
f"Assertion:n{stmt}nn"
f"STDOUT:n{p.stdout}nnSTDERR:n{p.stderr}"
)
return (t1 - t0, p.stdout.strip())
def summarize(instances: listing(float)) -> dict:
return {
"avg": sum(instances) / len(instances),
"min": min(instances),
"max": max(instances),
"runs": instances,
}
def bench_workload(identify: str, stmt: str) -> dict:
outcomes = {}
outputs = {}
for jit_val in (0, 1):
instances = ()
outs = ()
print(f" PYTHON_JIT={jit_val}: working {N_RUNS} instances...")
for i in vary(1, N_RUNS + 1):
dt, out = run_once(stmt, jit_val)
instances.append(dt)
outs.append(out)
print(f" run {i}/{N_RUNS}: {dt:.6f}s")
outcomes(jit_val) = summarize(instances)
outputs(jit_val) = outs
avg0 = outcomes(0)("avg")
avg1 = outcomes(1)("avg")
speedup = avg0 / avg1 if avg1 else float("inf")
delta_pct = (avg1 - avg0) / avg0 * 100.0 if avg0 else 0.0
return {
"workload": identify,
"jit0": outcomes(0),
"jit1": outcomes(1),
"speedup_jit0_over_jit1": speedup,
"delta_pct_jit1_vs_jit0": delta_pct,
"outputs": outputs, # sanity: must be steady
}
def principal() -> int:
all_results = ()
print(f"Utilizing Python: {PYTHON_EXE}")
print(f"Challenge dir: {PROJECT_DIR}")
print(f"Runs per setting (avg of all runs): {N_RUNS}n")
for identify, stmt in WORKLOADS:
print(f"=== {identify} ===")
r = bench_workload(identify, 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")
# Elective: warn if outputs differ throughout runs (nondeterminism)
if len(set(r("outputs")(0))) != 1:
print(" !! WARNING: JIT=0 output differs throughout runs (nondeterministic workload?)")
if len(set(r("outputs")(1))) != 1:
print(" !! WARNING: JIT=1 output differs throughout runs (nondeterministic workload?)")
OUTFILE.write_text(json.dumps(all_results, indent=2), encoding="utf-8")
print(f"Wrote: {OUTFILE}")
return 0
if __name__ == "__main__":
elevate SystemExit(principal())
Hier sind meine Ergebnisse.
C:Usersthomaprojectspython_jit>C:UsersthomaAppDataLocalProgramsPythonPython314python.exe benchmark.py
Utilizing Python: C:UsersthomaAppDataLocalProgramsPythonPython314python.exe
Challenge dir: C:Usersthomaprojectspython_jit
Runs per setting (avg of all runs): 10
=== mandelbrot ===
PYTHON_JIT=0: working 10 instances...
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: working 10 instances...
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: working 10 instances...
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: working 10 instances...
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: working 10 instances...
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: working 10 instances...
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%)
Interpretation der Ergebnisse
Wie Sie sehen, sind die Ergebnisse gemischt. Dies ist für ein experimentelles JIT regular.
- 10–30 % Beschleunigung: Häufig in „reinen Python“-Schleifen (wie den Mandelbrot- oder Levenshtein-Checks), bei denen die JIT den Overhead der Bytecode-Dispatch-Schleife vermeiden kann.
- 0 % Verbesserung: Häufig bei E/A-gebundenen Aufgaben oder Code, der häufig C-Erweiterungen verwendet. Der Dijkstra-Code wurde nicht schneller, da seine Laufzeit von Heap-/Tupeloperationen und speicherintensiver, zuordnungsgesteuerter Arbeit dominiert wird, die das aktuelle CPython-JIT nicht wesentlich optimiert, sodass etwaige Einsparungen durch den Interpreter im Lärm verloren gehen.
Wann sollte Python 3.14 JIT verwendet werden?
Das JIT ist ein leistungsstarkes Software, aber kein „Zauberknopf“. Meiner Erfahrung nach sollten Sie das JIT ausprobieren, wenn Sie …
- CPU-gebundene Logik: Ihre Anwendung führt umfangreiche Berechnungen, Datenverarbeitung oder komplexe Logik in reinem Python durch.
- Lang laufende Prozesse: Webserver (Gunicorn/Uvicorn) oder Hintergrundarbeiter (Celery), die stundenlang laufen, geben dem JIT genügend Zeit, sich aufzuwärmen und Scorching Paths zu optimieren.
- Experimentelle Checks: Sie möchten Ihre Codebasis für zukünftige Versionen von Python (3.15+) vorbereiten, bei denen die JIT wahrscheinlich aggressiver sein wird.
Und vermeiden Sie es, wenn Sie …
- E/A-gebundene Apps: Wenn Ihre App nur auf Datenbankabfragen oder API-Antworten wartet, hilft die JIT nicht.
- Umgebungen mit eingeschränktem Speicher: Kleine Lambda-Funktionen oder winzige Container können unter dem erhöhten Speicherbedarf des JIT-Cache leiden.
- Kurzlebige CLI-Instruments: Ein Skript, das in weniger als einer Sekunde ausgeführt wird, benötigt kein JIT.
Zukünftige Richtungen: Über 3.14 hinaus
Das CPython-Kernteam betrachtet 3.14 als „Gründungsjahr“. Zukünftige Iterationen (Python 3.15 und 3.16) werden voraussichtlich Folgendes umfassen:
- Tiefergehende Optimierungsdurchgänge: Nutzung der zur Laufzeit gesammelten Typinformationen, um eine noch aggressivere Maschinencodegenerierung durchzuführen.
- Bessere Heuristik: Intelligentere Entscheidungen über Wann zu kompilieren, wodurch die „Aufwärm“-Strafe reduziert wird.
- Geringerer Overhead: Verfeinerung des Kopier- und Patchmechanismus zur Reduzierung des Speicherverbrauchs.
Zusammenfassung
Der JIT von Python 3.14 ist mehr als nur ein Leistungspatch. Es ist eine Absichtserklärung. Es zeigt, dass Python es ernst meint, die Leistungslücke zu Sprachen wie Java oder Go zu schließen und gleichzeitig die „Batterien enthalten“-Einfachheit beizubehalten, die es berühmt gemacht hat.
Für die meisten Entwickler ist JIT einfach ein weiteres Software, das es wert ist, im Auge behalten zu werden. Wenn die Leistung in Ihren Projekten wichtig ist, lohnt es sich, Python 3.14 anhand Ihrer vorhandenen Arbeitslasten zu testen. Ein paar Benchmarks Ihrer wichtigsten Codepfade könnten Leistungssteigerungen aufdecken, wo Sie sie nicht erwartet haben.
Hier ist der Hyperlink zu meinem vorherigen Artikel über GIL Payment Python, den ich zu Beginn erwähnt habe.
