Pythons JIT-Compiler kommt endlich
Python 3.15 bringt einen Copy-and-Patch-JIT-Compiler. Wie er funktioniert, welche Speedups zu erwarten sind und warum CPython so lange gebraucht hat.

Python gilt schon so lange als „zu langsam“, wie es Python gibt. Die Standardantwort der Python-Community – „für die heißen Schleifen einfach C-Erweiterungen nutzen“ – war immer ein Zugeständnis daran, dass das Ausführungsmodell der Sprache grundsätzlich limitiert ist. CPython interpretiert Bytecode Anweisung für Anweisung, wobei jede Instruktion über ein switch-Statement verteilt wird. Das ist einfach, portabel und leicht zu debuggen. Bei rechenintensiven Aufgaben ist es aber auch etwa 100-mal langsamer als kompiliertes C.
Python 3.15 ändert das. Nach jahrelanger Experimentierarbeit wird der Copy-and-Patch-JIT-Compiler als standardmäßig aktiviertes Feature ausgeliefert. Python wird dadurch nicht so schnell wie C – das schafft ohne statische Kompilierung ohnehin niemand –, aber die ersten Benchmarks zeigen 15 bis 30 Prozent Speedup bei realem Code, bestimmte Muster profitieren sogar deutlich stärker. Für eine Sprache, deren Performance-Geschichte seit 30 Jahren darin besteht, den heißen Pfad einfach in C neu zu schreiben, ist ein JIT, der reinen Python-Code spürbar beschleunigt, ein echter Meilenstein.
Warum CPython nie einen JIT hatte
Das liegt nicht daran, dass es niemand versucht hätte. PyPy hat seit über zehn Jahren einen JIT und läuft regelmäßig 5- bis 10-mal schneller als CPython. PyPy ist aber eine eigenständige Implementierung mit eigener Laufzeitumgebung, und sie hat nie CPythons Marktanteil erreicht, weil das C-Erweiterungs-Ökosystem – NumPy, pandas, scikit-learn, alles, was Python zur Sprache der Datenwissenschaft macht – an die C-API von CPython gebunden ist.
Einen JIT direkt in CPython einzubauen, wurde schon mehrfach diskutiert und versucht. Die Hürden sind gut dokumentiert. Die Architektur von CPython erschwert die JIT-Kompilierung: Der Bytecode ist dynamisch typisiert (ein JIT braucht Typinformationen, um effizienten Code zu erzeugen, und Python liefert sie nicht statisch), die C-API erlaubt C-Code, Python-Objekte direkt zu manipulieren, und das Reference Counting des Garbage Collectors erzeugt Buchhaltungsaufwand, den ein JIT nur schwer eliminieren kann.
Frühere Ansätze – Unladen Swallow (Google, 2009) und Pyston (Dropbox, 2014) – versuchten, einen LLVM-basierten JIT an CPython anzuflanschen. Beide stellten fest, dass der Kompilierungsaufwand von LLVM für typische Python-Workloads zu hoch war. LLVM ist für die Ahead-of-Time-Kompilierung großer Codebasen gemacht; wenn man damit kurze Python-Funktionen zur Laufzeit kompiliert, kostet das Millisekunden Kompilierzeit für Mikrosekunden Ausführungszeit. Die Kompilierung war teurer als der Speedup.
Copy-and-Patch: Ein anderer Typ von JIT
Die Copy-and-Patch-Technik, die in einem Forschungspapier von 2021 vorgestellt wurde, verfolgt einen grundlegend anderen Ansatz zur JIT-Kompilierung. Statt Bytecode in eine Zwischenrepräsentation zu übersetzen und Optimierungspässe laufen zu lassen (der LLVM-Weg), arbeitet Copy-and-Patch mit vorkompilierten Code-Templates.
Die Idee dahinter: Für jede Bytecode-Instruktion (LOAD_FAST, BINARY_ADD, CALL_FUNCTION usw.) wird eine C-Implementierung vorab in Maschinencode kompiliert, mit Platzhaltern („Löchern“) für variable Daten – Registerzuweisungen, Konstantenwerte, Speicheroffsets. Zur Laufzeit besteht die JIT-Kompilierung dann nur noch daraus, das vorkompilierte Template zu kopieren und die Löcher mit den konkreten Werten für diese Funktion zu füllen. Keine Optimierungspässe, keine Registerallokationsalgorithmen, keine Instruktionsauswahl. Nur memcpy und Patch.
Traditional JIT pipeline:
Python bytecode
→ Parse into IR
→ Type inference
→ Optimization passes (CSE, DCE, loop unrolling, inlining...)
→ Register allocation
→ Instruction selection
→ Machine code
Total: 1-100ms per function
Copy-and-patch pipeline:
Python bytecode
→ For each instruction, copy pre-compiled template
→ Fill in holes (constants, offsets)
→ Done — machine code ready
Total: 1-100μs per function (1000x faster compilation)
Der Preis ist die Code-Qualität. LLVM erzeugt hochoptimierten Maschinencode. Copy-and-Patch erzeugt im Wesentlichen eine kompilierte Version der Interpreter-Schleife – jede Bytecode-Instruktion ist weiterhin ein eigenes Template, mit minimaler Optimierung über Instruktionsgrenzen hinweg. Der erzeugte Code ist besser als Interpretation (kein Dispatch-Overhead, kein switch-Statement, bessere Branch Prediction), aber schlechter als das, was ein vollwertiger optimierender Compiler liefern würde.
Für Python ist dieser Trade-off hervorragend. Python-Funktionen sind typischerweise kurz, werden oft aufgerufen und dauern einzeln Mikrosekunden. Ein JIT, der in Mikrosekunden kompiliert und die Ausführung um 20 bis 30 Prozent beschleunigt, ist wertvoller als einer, der in Millisekunden kompiliert und die Ausführung um 200 Prozent beschleunigt – weil sich der Kompilierungsaufwand beim ersten Ansatz fast sofort amortisiert.
Was schneller wird
Der JIT beschleunigt nicht magischerweise alle Python-Programme gleichermaßen. Um zu verstehen, was am meisten profitiert, muss man wissen, wofür der Interpreter seine Zeit aufwendet.
Bytecode-Dispatch-Overhead. Im Interpreter braucht jede Bytecode-Instruktion Folgendes: den nächsten Opcode holen, dekodieren und über ein switch-Statement zum Handler springen. Dieser Dispatch-Overhead kann bei engen Schleifen 30 bis 50 Prozent der Gesamtlaufzeit ausmachen. Der JIT eliminiert ihn komplett – die Instruktionen werden zu sequenziellem Maschinencode mit direkten Sprüngen kompiliert.
Typspezialisierte Operationen. Python 3.11 führte den spezialisierenden adaptiven Interpreter ein, der generische Operationen durch typspezifische ersetzt, nachdem er die tatsächlich verwendeten Typen beobachtet hat. BINARY_ADD wird zu BINARY_ADD_INT, wenn zwei Ganzzahlen auftauchen. Der JIT kompiliert diese spezialisierten Instruktionen in effizienten Maschinencode – eine Ganzzahladdition wird zu einer einzigen add-Instruktion statt eines Funktionsaufrufs.
Branch Prediction. Die zentrale Dispatch-Schleife des Interpreters – ein switch mit Hunderten von Fällen – ist ein Albtraum für den Branch Predictor der CPU. Der JIT ersetzt sie durch direkten Kontrollfluss, den die CPU zuverlässig vorhersagt. Auf modernen CPUs, bei denen Fehlvorhersagen 15 bis 20 Zyklen kosten, macht allein das einen erheblichen Teil des Speedups aus.
Was nicht schneller wird: C-Erweiterungsaufrufe (NumPy, pandas), I/O-Operationen (Netzwerk, Festplatte) und Operationen, die von Speicherallokation dominiert werden (das Erzeugen Millionen kleiner Objekte). Wenn dein Python-Programm 95 Prozent der Zeit in C-Erweiterungen verbringt und 5 Prozent in reinem Python, beschleunigt der JIT diese 5 Prozent – messbar, aber nicht transformativ.
Die Spezialisierungs-Pipeline
Der JIT arbeitet nicht allein. Er ist die letzte Stufe einer Performance-Pipeline, die mit dem spezialisierenden Interpreter von Python 3.11 begann und sich über die inkrementellen Verbesserungen in 3.12 bis 3.14 fortsetzte.
- Stufe 0: Interpreter. Jeder Code beginnt hier. Standard-Bytecode-Interpretation mit adaptiver Spezialisierung. Nachdem eine Funktion mehrmals aufgerufen wurde, werden heiße Instruktionen durch typspezialisierte Varianten ersetzt.
- Stufe 1: JIT-kompilierter Bytecode. Der Copy-and-Patch-JIT kompiliert den spezialisierten Bytecode in Maschinencode. Das eliminiert den Dispatch-Overhead und ermöglicht grundlegende Optimierungen wie Konstantenfaltung und Entfernung von totem Code innerhalb der kompilierten Templates.
- Stufe 2 (Zukunft): Trace-basierte Optimierung. Ausführungspfade durch heißen Code werden aufgezeichnet und ganze Traces – über Funktionsgrenzen hinweg – in optimierten Maschinencode kompiliert. Das ist geplant, aber noch nicht ausgeliefert.
Dieser gestufte Ansatz ähnelt dem, was Rubys YJIT und andere moderne Laufzeitumgebungen für Sprachen nutzen. Man startet mit schneller Interpretation, wechselt zu schneller Kompilierung, sobald Code heiß wird, und hebt sich die teure Optimierung für die heißesten Pfade auf. Die Grundidee ist dieselbe: Die meisten Codes lohnen die Optimierung nicht, also gibt man das Kompilierbudget für den Code aus, der am häufigsten läuft.
Auswirkungen auf Speicher und Startzeit
JIT-Compiler verbrauchen Speicher für kompilierten Code. Der Speicherbedarf des Copy-and-Patch-JIT ist moderat – kompilierter Code ist größer als Bytecode, aber kleiner als das, was LLVM-basierte JITs erzeugen (weil es keinen Optimierungs-Ballast gibt). Die aktuelle Implementierung benötigt etwa das 1,5- bis 3-Fache des Speichers des ersetzten Bytecodes und kompiliert nur Funktionen, die oft genug aufgerufen werden, um davon zu profitieren.
Die Startzeit ist ein Problem für kurzlebige Python-Skripte. Der JIT verursacht Mehraufwand beim Laden der Template-Bibliothek und beim Einrichten der Kompilierungsinfrastruktur. Bei Skripten, die weniger als eine Sekunde laufen, kann dieser Mehraufwand den Speedup übersteigen. CPython begegnet dem, indem Funktionen erst nach einer Schwelle an Aufrufen kompiliert werden – kurzlebige Skripte bleiben im Interpreter und zahlen keine JIT-Abgabe.
Das lässt sich konfigurieren. Das Flag -X jit steuert das JIT-Verhalten, und Umgebungsvariablen passen die Kompilierungsschwelle an. Für Serverless-Funktionen und CLI-Tools, bei denen der Start zählt, kannst du die Schwelle erhöhen oder den JIT ganz deaktivieren. Für lang laufende Server und Datenverarbeitungsskripte, bei denen die Performance im Dauerbetrieb zählt, funktionieren die Standardwerte gut.
Was das für das Python-Ökosystem bedeutet
Der JIT ändert nichts an Pythons Platz in der Performance-Hierarchie – C, Rust, Go und Java sind bei rechenintensiven Aufgaben nach wie vor deutlich schneller. Was sich ändert, ist die Schwelle, ab der Python-Entwickler zu diesen Alternativen greifen müssen.
Ein Speedup von 20 bis 30 Prozent bei reinem Python-Code bedeutet, dass manche Workloads, die bisher C-Erweiterungen oder eine Neuimplementierung brauchten, nun in reinem Python schnell genug laufen. Datenverarbeitungsskripte, die 10 Minuten brauchten, brauchen 7. Webserver, die 1000 Requests pro Sekunde schafften, schaffen 1300. Das sind keine revolutionären Zahlen, aber sie machen den Unterschied zwischen „Python ist schnell genug“ und „wir müssen das in Go neu schreiben“.
Wichtiger noch: Die JIT-Infrastruktur legt das Fundament für künftige Optimierungen. Der Copy-and-Patch-Ansatz lässt sich mit besseren Templates, mehr Spezialisierung und irgendwann trace-basierter Kompilierung erweitern. Jede Python-Version kann bessere Templates ausliefern, ohne die grundlegende Architektur des JIT zu ändern. Der Speedup von 20 bis 30 Prozent in 3.15 ist ein Boden, keine Decke.
Nach drei Jahrzehnten als eine der beliebtesten und zugleich langsamsten Sprachen der Welt investiert CPython endlich ernsthaft in Performance. Der JIT wird die Fraktion „Python ist zu langsam“ nicht zufriedenstellen – das wird nichts, denn für manche Workloads ist Python tatsächlich zu langsam, mit oder ohne JIT. Aber für den Großteil des Python-Codes, bei dem die Ausführungsgeschwindigkeit „okay, aber nicht toll“ war, bringt der JIT ihn näher an „wirklich gut“. Das ist eine größere Sache, als es klingt.


