Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 744 – JIT-Kompilierung

Autor:
Brandt Bucher <brandt at python.org>, Savannah Ostrowski <savannah at python.org>
Discussions-To:
Discourse thread
Status:
Entwurf
Typ:
Informational
Erstellt:
11. April 2024
Python-Version:
3.13
Post-History:
11. April 2024

Inhaltsverzeichnis

Zusammenfassung

Anfang dieses Jahres wurde ein experimenteller „Just-in-Time“-Compiler in den Hauptentwicklungszweig von CPython (main) integriert. Während neuere CPython-Versionen andere wesentliche interne Änderungen enthalten haben, stellt diese Ergänzung eine besonders signifikante Abweichung von der Art und Weise dar, wie CPython traditionell Python-Code ausführt. Daher verdient sie eine breitere Diskussion.

Dieses PEP zielt darauf ab, die Designentscheidungen hinter dieser Ergänzung, den aktuellen Stand der Implementierung und zukünftige Pläne zur dauerhaften, nicht-experimentellen Integration des JIT in CPython zusammenzufassen. Es versucht *nicht*, einen umfassenden Überblick darüber zu geben, *wie* der JIT funktioniert, sondern konzentriert sich stattdessen auf die spezifischen Vor- und Nachteile des gewählten Ansatzes sowie auf die Beantwortung vieler Fragen, die seit seiner Einführung zum JIT gestellt wurden.

Leser, die mehr über den neuen JIT erfahren möchten, werden ermutigt, die folgenden Ressourcen zu konsultieren:

  • Die Präsentation, die den JIT erstmals auf dem CPython Core Developer Sprint 2023 vorstellte. Sie enthält relevante Hintergrundinformationen, eine leichte technische Einführung in die verwendete „Copy-and-Patch“-Technik und eine offene Diskussion seines Designs unter den anwesenden Kernentwicklern. Folien zu diesem Vortrag finden Sie auf GitHub.
  • Das Open-Access-Paper, das ursprünglich Copy-and-Patch beschrieb.
  • Der Blogbeitrag des Autors des Papers, der die Implementierung eines Copy-and-Patch JIT-Compilers für Lua detailliert beschreibt. Obwohl dies eine großartige Low-Level-Erklärung des Ansatzes ist, beachten Sie, dass er auch andere Techniken integriert und Implementierungsentscheidungen trifft, die für den CPython JIT nicht besonders relevant sind.
  • Die Referenzimplementierung selbst.

Motivation

Bis zu diesem Punkt hat CPython Python-Code immer durch Kompilierung in Bytecode ausgeführt, der zur Laufzeit interpretiert wird. Dieser Bytecode ist eine mehr oder weniger direkte Übersetzung des Quellcodes: er ist untypisiert und weitgehend unoptimiert.

Seit der Veröffentlichung von Python 3.11 verwendet CPython einen „spezialisierenden adaptiven Interpreter“ (PEP 659), der diese Bytecode-Instruktionen während der Ausführung inplace mit typspezialisierten Versionen überschreibt. Dieser neue Interpreter liefert signifikante Leistungsverbesserungen, obwohl sein Optimierungspotenzial durch die Grenzen einzelner Bytecode-Instruktionen begrenzt ist. Er sammelt auch eine Fülle neuer Profiling-Informationen: die durch ein Programm fließenden Typen, das Speicherlayout bestimmter Objekte und welche Pfade durch das Programm am häufigsten ausgeführt werden. Mit anderen Worten: *was* optimiert werden soll und *wie* es optimiert werden soll.

Seit der Veröffentlichung von Python 3.12 generiert CPython diesen Interpreter aus einer C-ähnlichen domänenspezifischen Sprache (DSL). Neben der Bändigung einiger der Komplexität des neuen adaptiven Interpreters ermöglicht die DSL auch CPython-Maintainern, das manuelle Schreiben mühsamer Boilerplate-Codes in vielen Teilen des Interpreters, Compilers und der Standardbibliothek zu vermeiden, die mit den Instruktionsdefinitionen synchron gehalten werden müssen. Diese Fähigkeit, große Mengen an Laufzeuginfrastruktur aus einer einzigen Quelle der Wahrheit zu generieren, ist nicht nur für die Wartung praktisch, sondern eröffnet auch viele Möglichkeiten zur Erweiterung der Ausführung von CPython auf neue Weise. Zum Beispiel macht sie es machbar, automatisch Tabellen für die Übersetzung einer Sequenz von Instruktionen in eine äquivalente Sequenz kleinerer „Mikro-Ops“ zu generieren, einen Optimierer für Sequenzen dieser Mikro-Ops zu generieren und sogar einen vollständigen zweiten Interpreter für deren Ausführung zu generieren.

Tatsächlich enthalten seit Anfang des Python 3.13 Release-Zyklus alle CPython-Builds exakt diese Maschinen für die Mikro-Op-Übersetzung, Optimierung und Ausführung. Sie sind jedoch standardmäßig deaktiviert; der Overhead der Interpretation selbst optimierter Mikro-Op-Traces ist für die meisten Codes einfach zu groß. Eine stärkere Optimierung wird die Situation wahrscheinlich auch nicht wesentlich verbessern, da alle durch neue Optimierungen erzielten Effizienzgewinne wahrscheinlich durch den interpretativen Overhead noch kleinerer, komplexerer Mikro-Ops aufgewogen werden.

Die offensichtlichste Strategie, diesen neuen Engpass zu überwinden, ist die statische Kompilierung dieser optimierten Traces. Dies bietet die Möglichkeit, mehrere Quellen für Indirektion und Overhead zu vermeiden, die durch die Interpretation eingeführt werden. Insbesondere ermöglicht es die Beseitigung von Dispatch-Overhead zwischen Mikro-Ops (durch Ersetzung eines generischen Interpreters durch eine lineare Sequenz von Hot Code), Instruktionsdekodierungs-Overhead für einzelne Mikro-Ops (durch „Einbrennen“ der Werte oder Adressen von Argumenten, Konstanten und gecachten Werten direkt in Maschinencode-Instruktionen) und Speicherverkehr (durch Verschieben von Daten von Heap-allozierten Python-Frames in physische Hardware-Register).

Da ein Großteil dieser Daten selbst zwischen identischen Ausführungen eines Programms variiert und die bestehende Optimierungspipeline stark auf Laufzeit-Profiling-Informationen setzt, macht es wenig Sinn, diese Traces im Voraus zu kompilieren, und es wäre eine substanzielle Neugestaltung der bestehenden Spezifikation und der Mikro-Op-Tracing-Infrastruktur, die bereits implementiert wurde. Wie für viele andere dynamische Sprachen (und sogar für Python selbst) gezeigt wurde, ist der vielversprechendste Ansatz, die optimierten Mikro-Ops „just in time“ für die Ausführung zu kompilieren.

Begründung

Trotz ihres Rufs sind JIT-Compiler keine magischen „Schneller machen“-Maschinen. Die Entwicklung und Wartung jeglicher Art von optimierendem Compiler, selbst für eine einzige Plattform, geschweige denn für alle gängigsten unterstützten Plattformen von CPython, ist eine unglaublich komplizierte und kostspielige Aufgabe. Die Verwendung eines bestehenden Compiler-Frameworks wie LLVM kann diese Aufgabe vereinfachen, aber nur auf Kosten von schweren Laufzeitabhängigkeiten und deutlich höherem JIT-Kompilierungs-Overhead.

Es ist klar, dass die erfolgreiche Kompilierung von Python-Code zur Laufzeit nicht nur hochwertige Python-spezifische Optimierungen für den ausgeführten Code erfordert, *sondern auch* eine schnelle Generierung von effizientem Maschinencode für das optimierte Programm. Das Python-Kernentwicklungsteam verfügt über die notwendigen Fähigkeiten und Erfahrungen für ersteres (eine Middle-End-Schicht, die eng mit dem Interpreter gekoppelt ist), und die Copy-and-Patch-Kompilierung bietet eine attraktive Lösung für letzteres.

Kurz gesagt, Copy-and-Patch ermöglicht die Generierung eines hochwertigen JIT-Compiler-Templates aus derselben DSL, die zur Generierung des restlichen Interpreters verwendet wird. Für ein weit verbreitetes, ehrenamtlich geführtes Projekt wie CPython kann dieser Vorteil nicht hoch genug eingeschätzt werden: CPython-Maintainer erhalten durch bloßes Bearbeiten der Bytecode-Definitionen auch den JIT-Backend-Update „kostenlos“ für *alle* JIT-unterstützten Plattformen gleichzeitig. Dies gilt gleichermaßen, wenn Instruktionen hinzugefügt, geändert oder entfernt werden.

Wie der Rest des Interpreters wird auch der JIT-Compiler zur Build-Zeit generiert und hat keine Laufzeitabhängigkeiten. Er unterstützt eine breite Palette von Plattformen (siehe Abschnitt Support unten) und hat einen vergleichsweise geringen Wartungsaufwand. Insgesamt besteht die aktuelle Implementierung aus etwa 900 Zeilen Python-Code für die Build-Zeit und 500 Zeilen C-Code für die Laufzeit.

Spezifikation

Der JIT ist derzeit nicht Teil der Standard-Build-Konfiguration und wird dies wahrscheinlich auch in absehbarer Zeit bleiben (offizielle Binärdateien könnten ihn jedoch enthalten). Dennoch wird der JIT nicht-experimentell, sobald alle folgenden Bedingungen erfüllt sind:

  1. Er liefert eine sinnvolle Leistungssteigerung für mindestens eine populäre Plattform (realistisch gesehen im Bereich von 5%).
  2. Er kann mit minimaler Störung gebaut, verteilt und bereitgestellt werden.
  3. Der Steering Council hat auf Anfrage festgestellt, dass die Aktivierung der JIT der Community mehr Wert bietet als ihre Deaktivierung (unter Berücksichtigung von Kompromissen wie Wartungsaufwand, Speicherverbrauch oder der Machbarkeit alternativer Designs).

Diese Kriterien sollten als Ausgangspunkt betrachtet werden und können im Laufe der Zeit erweitert werden. Beispielsweise könnten bei der Diskussion dieses PEP zusätzliche Anforderungen (wie mehrere engagierte Maintainer, eine Sicherheitsprüfung, Dokumentation im Devguide, Unterstützung für Out-of-Process-Debugging oder eine Laufzeitoption zur Deaktivierung des JIT) zu dieser Liste hinzugefügt werden.

Bis der JIT nicht-experimentell ist, sollte er *nicht* in der Produktion verwendet werden und kann jederzeit ohne Vorwarnung gebrochen oder entfernt werden.

Sobald der JIT nicht mehr experimentell ist, sollte er weitgehend so behandelt werden wie andere Build-Optionen wie --enable-optimizations oder --with-lto. Er kann eine empfohlene (oder sogar Standard-)Option für einige Plattformen sein, und Release-Manager *können* sich entscheiden, ihn in offizielle Releases aufzunehmen.

Unterstützung

Der JIT wurde für alle aktuellen Tier-1-Plattformen von PEP 11, die meisten seiner Tier-2-Plattformen und eine seiner Tier-3-Plattformen entwickelt. Insbesondere kompiliert und testet der CI-Zweig main von CPython den JIT sowohl für Release- als auch für Debug-Builds auf:

  • aarch64-apple-darwin/clang
  • aarch64-pc-windows/msvc [1]
  • aarch64-unknown-linux-gnu/clang [2]
  • aarch64-unknown-linux-gnu/gcc [2]
  • i686-pc-windows-msvc/msvc
  • x86_64-apple-darwin/clang
  • x86_64-pc-windows-msvc/msvc
  • x86_64-unknown-linux-gnu/clang
  • x86_64-unknown-linux-gnu/gcc

Es ist erwähnenswert, dass einige Plattformen, selbst zukünftige Tier-1-Plattformen, möglicherweise nie JIT-Unterstützung erhalten werden. Dies kann aus verschiedenen Gründen geschehen, darunter unzureichende LLVM-Unterstützung (powerpc64le-unknown-linux-gnu/gcc), inhärente Einschränkungen der Plattform (wasm32-unknown-wasi/clang) oder mangelndes Entwicklerinteresse (x86_64-unknown-freebsd/clang).

Sobald die JIT-Unterstützung für eine Plattform hinzugefügt wurde (d. h. der JIT erfolgreich ohne Warnungen für den Benutzer kompiliert), sollte sie weitgehend so behandelt werden, wie es PEP 11 vorschreibt: Sie sollte zuverlässige CI/Buildbots haben, und JIT-Fehler auf Tier-1- und Tier-2-Plattformen sollten Releases blockieren. Obwohl es nicht notwendig ist, PEP 11 zur Angabe der JIT-Unterstützung zu aktualisieren, kann es dennoch hilfreich sein. Andernfalls sollte eine Liste der unterstützten Plattformen in der README-Datei des JIT gepflegt werden.

Da es immer möglich sein sollte, CPython ohne den JIT zu bauen, sollte die Entfernung der JIT-Unterstützung für eine Plattform *nicht* als rückwärtskompatibler Bruch betrachtet werden. Wenn es jedoch vernünftig ist, sollte der normale Deprozess gemäß PEP 387 befolgt werden.

Die Build-Zeit-Abhängigkeiten des JIT können innerhalb vernünftiger Grenzen zwischen den Releases geändert werden.

Abwärtskompatibilität

Aufgrund der Tatsache, dass der aktuelle Interpreter und der JIT-Backend beide aus derselben Spezifikation generiert werden, sollte das Verhalten von Python-Code vollständig unverändert bleiben. In der Praxis waren beobachtbare Unterschiede, die während des Tests gefunden und behoben wurden, eher Fehler in den bestehenden Mikro-Op-Übersetzungs- und Optimierungsstufen als Fehler im Copy-and-Patch-Schritt.

Debugging

Tools, die Python-Code profilieren und debuggen, funktionieren weiterhin einwandfrei. Dazu gehören In-Process-Tools, die Python-Funktionalität nutzen (wie sys.monitoring, sys.settrace oder sys.setprofile), sowie Out-of-Process-Tools, die Python-Frames aus dem Interpreter-Zustand durchlaufen.

Es scheint jedoch, dass Profiler und Debugger *für C-Code* derzeit nicht in der Lage sind, durch JIT-Frames zurückzuverfolgen. Die Arbeit mit Leaf-Frames ist möglich (so wird der JIT selbst debuggt), obwohl sie aufgrund des Fehlens korrekter Debugging-Informationen für JIT-Frames von begrenztem Nutzen ist.

Da die vom JIT ausgegebenen Code-Vorlagen von Clang kompiliert werden, *könnte* es möglich sein, das Tracing von JIT-Frames zu ermöglichen, indem einfach die Compiler-Flags so geändert werden, dass Frame-Pointer sorgfältiger verwendet werden. Es könnte auch möglich sein, die von Clang erzeugten Debugging-Informationen zu sammeln und auszugeben. Keine dieser Ideen wurde bisher sehr tiefgehend untersucht.

Obwohl dies ein Problem ist, das behoben *werden sollte*, hat die Behebung derzeit keine besonders hohe Priorität. Dies ist wahrscheinlich ein Problem, das von jemandem mit mehr Fachkenntnissen in Zusammenarbeit mit denjenigen, die den JIT warten und wenig Erfahrung mit den inneren Abläufen dieser Tools haben, am besten untersucht werden kann.

Sicherheitsimplikationen

Dieser JIT erzeugt, wie jeder JIT, zur Laufzeit große Mengen ausführbaren Daten. Dies führt zu einer potenziellen neuen Angriffsfläche für CPython, da ein böswilliger Akteur, der in der Lage ist, den Inhalt dieser Daten zu beeinflussen, dadurch in der Lage ist, beliebigen Code auszuführen. Dies ist eine bekannte Schwachstelle von JIT-Compilern.

Um dieses Risiko zu mindern, wurde der JIT nach Best Practices geschrieben. Insbesondere werden die fraglichen Daten vom JIT-Compiler nicht an andere Teile des Programms weitergegeben, solange sie schreibbar sind, und zu *keinem* Zeitpunkt sind die Daten sowohl schreibbar *als auch* ausführbar.

Die Natur von Template-basierten JITs begrenzt zudem stark die Art des generierbaren Codes, was die Wahrscheinlichkeit eines erfolgreichen Exploits weiter reduziert. Als zusätzliche Vorsichtsmaßnahme werden die Templates selbst im statischen, schreibgeschützten Speicher gespeichert.

Es wäre jedoch naiv anzunehmen, dass keine möglichen Schwachstellen im JIT existieren, insbesondere in diesem frühen Stadium. Der Autor ist kein Sicherheitsexperte, steht aber zur Verfügung, um dem Python Security Response Team beizutreten oder eng mit ihm zusammenzuarbeiten, um Sicherheitsprobleme bei deren Auftreten zu triagieren und zu beheben.

Apple Silicon

Obwohl schwer zu testen, ohne eine macOS-Version tatsächlich zu signieren und zu verpacken, *scheint* es, dass macOS-Releases das JIT-Entitlement für die Hardened Runtime aktivieren sollten.

Dies sollte die *Installation* von Python nicht erschweren, kann aber zusätzliche Schritte für Release-Manager bedeuten.

Wie man das lehrt

Wählen Sie die Abschnitte, die Sie am besten beschreiben

  • Wenn Sie ein Python-Programmierer oder Endbenutzer sind...
    • ...ändert sich für Sie nichts. Niemand sollte Ihnen JIT-aktivierte CPython-Interpreter verteilen, solange es sich noch um eine experimentelle Funktion handelt. Sobald sie nicht-experimentell ist, werden Sie wahrscheinlich eine etwas bessere Leistung und einen etwas höheren Speicherverbrauch feststellen. Sie sollten keine anderen Änderungen beobachten können.
  • Wenn Sie Drittanbieterpakete pflegen...
    • ...ändert sich für Sie nichts. Es gibt keine API- oder ABI-Änderungen, und der JIT ist für Drittanbietercode nicht zugänglich. Sie sollten Ihre CI-Matrix nicht ändern müssen und keine Unterschiede in der Funktionsweise Ihrer Pakete beobachten können, wenn der JIT aktiviert ist.
  • Wenn Sie Python-Code profilieren oder debuggen...
    • ...ändert sich für Sie nichts. Alle Python-Profiling- und Tracing-Funktionen bleiben erhalten.
  • Wenn Sie C-Code profilieren oder debuggen...
    • ...derzeit ist die Möglichkeit, *durch* JIT-Frames zu tracen, eingeschränkt. Dies kann zu Problemen führen, wenn Sie den gesamten C-Call-Stack beobachten müssen und nicht nur „Leaf“-Frames. Weitere Informationen finden Sie im Abschnitt Debugging oben.
  • Wenn Sie Ihren eigenen Python-Interpreter kompilieren...
    • ...wenn Sie den JIT nicht bauen möchten, können Sie ihn einfach ignorieren. Andernfalls müssen Sie eine kompatible Version von LLVM installieren und das entsprechende Flag an die Build-Skripte übergeben. Ihr Build kann bis zu einer Minute länger dauern. Beachten Sie, dass der JIT *nicht* an Endbenutzer verteilt oder in der Produktion verwendet werden sollte, solange er sich noch in der experimentellen Phase befindet.
  • Wenn Sie ein CPython-Maintainer sind (oder eines Forks von CPython)...
    • ...und Sie die Bytecode-Definitionen oder die Haupt-Interpreter-Schleife ändern...
      • ...im Allgemeinen sollte der JIT keine große Unannehmlichkeit für Sie darstellen (abhängig davon, was Sie tun möchten). Der Mikro-Op-Interpreter wird nirgendwohin gehen und bietet immer noch eine Debugging-Erfahrung, die der heutigen Haupt-Bytecode-Interpreter-Erfahrung ähnelt. Es besteht eine moderate Wahrscheinlichkeit, dass größere Änderungen am Interpreter (wie das Hinzufügen neuer lokaler Variablen, Änderungen an der Fehlerbehandlung und Deoptimierungslogik oder Änderungen am Mikro-Op-Format) Änderungen am C-Template erfordern, das zur Generierung des JIT verwendet wird und die Haupt-Interpreter-Schleife nachahmen soll. Sie können auch gelegentlich Pech haben und die JIT-Code-Generierung brechen, was Sie entweder dazu zwingt, die Python-Build-Skripte selbst zu ändern, oder Hilfe von jemandem zu suchen, der damit vertrauter ist (siehe oben).
    • ...und Sie am JIT selbst arbeiten...
      • ...Sie haben hoffentlich bereits eine gute Vorstellung davon, worauf Sie sich einlassen. Sie werden regelmäßig die Python-Build-Skripte, das C-Template zur Generierung des JIT und den C-Code, der den Laufzeitanteil des JIT tatsächlich ausmacht, bearbeiten. Sie werden sich auch mit allen möglichen Abstürzen befassen, Maschinencode in einem Debugger überspringen, COFF/ELF/Mach-O-Dumps betrachten, auf einer breiten Palette von Plattformen entwickeln und im Allgemeinen der Ansprechpartner für die Leute sein, die den Bytecode ändern, wenn die CI bei ihren PRs fehlschlägt (siehe oben). Idealerweise sind Sie zumindest mit Assembler *vertraut*, haben ein paar Kurse mit „Compiler“ im Namen besucht und einen Blogbeitrag oder zwei über Linker gelesen.
    • ...und Sie pflegen andere Teile von CPython...
      • ...für Sie ändert sich nichts. Sie müssen nicht lokal mit JIT-Builds entwickeln. Wenn Sie sich dafür entscheiden (z. B. um JIT-Probleme zu reproduzieren und zu triagieren), können Ihre Builds jedes Mal, wenn die relevanten Dateien geändert werden, bis zu einer Minute länger dauern.

Referenzimplementierung

Schlüsselkomponenten der Implementierung umfassen:

Abgelehnte Ideen

Außerhalb von CPython pflegen

Obwohl es *wahrscheinlich* möglich ist, den JIT außerhalb von CPython zu pflegen, ist seine Implementierung so eng mit dem Rest des Interpreters verknüpft, dass die Aktualisierung wahrscheinlich schwieriger wäre als die tatsächliche Entwicklung des JIT selbst. Zusätzlich müssten Mitwirkende, die an den bestehenden Mikro-Op-Definitionen und Optimierungen arbeiten, zwei separate Projekte modifizieren und bauen, um die Auswirkungen ihrer Änderungen unter dem JIT zu messen (wohingegen heute die Infrastruktur existiert, um dies automatisch für jede vorgeschlagene Änderung zu tun).

Releases des separaten „JIT“-Projekts müssten wahrscheinlich auch bestimmten CPython-Vorab- und Patch-Releases entsprechen, je nachdem, welche Änderungen genau vorliegen. Einzelne CPython-Commits zwischen Releases hätten wahrscheinlich überhaupt keine entsprechenden JIT-Releases, was die Fehlersuche erschweren würde (z. B. Bisection zur Identifizierung von Breaking Changes upstream).

Da der JIT bereits recht stabil ist und das Endziel darin besteht, dass er ein nicht-experimenteller Teil von CPython wird, scheint es der beste Weg zu sein, ihn in main zu belassen. Nichtsdestotrotz ist der relevante Code so organisiert, dass der JIT leicht „gelöscht“ werden kann, falls er seine Ziele nicht erreicht.

Standardmäßig aktivieren

Andererseits wurde vorgeschlagen, dass der JIT in seiner aktuellen Form standardmäßig aktiviert werden sollte.

Nochmal, es ist wichtig zu bedenken, dass ein JIT keine magische „Schneller machen“-Maschine ist; derzeit ist der JIT etwa so schnell wie der bestehende spezialisierende Interpreter. Das mag unterwältigend klingen, ist aber tatsächlich eine ziemlich bedeutende Leistung, und es ist der Hauptgrund, warum dieser Ansatz als tragfähig genug erachtet wurde, um zur weiteren Entwicklung in main integriert zu werden.

Obwohl der JIT signifikante Vorteile gegenüber dem bestehenden Mikro-Op-Interpreter bietet, ist er noch kein klarer Gewinn, wenn er immer aktiviert ist (insbesondere angesichts seines erhöhten Speicherverbrauchs und zusätzlicher Build-Zeit-Abhängigkeiten). Das ist der Zweck dieses PEP: Erwartungen bezüglich der objektiven Kriterien zu klären, die erfüllt sein müssen, um den Schalter umzulegen.

Zumindest vorerst scheint es ein guter Kompromiss zu sein, dies in main zu haben, aber standardmäßig deaktiviert zu lassen, anstatt es immer zu aktivieren oder gar nicht verfügbar zu haben.

Mehrere Compiler-Toolchains unterstützen

Clang wird speziell benötigt, da es der einzige C-Compiler ist, der die garantierte Unterstützung für Tail Calls (musttail) bietet, die für CPythons Continuation-Passing-Style Ansatz zur JIT-Kompilierung erforderlich sind. Ohne ihn könnten die Tail-Recursive-Aufrufe zwischen Templates zu unbegrenztem C-Stack-Wachstum (und schließlich Überlauf) führen.

Da LLVM auch andere für den JIT-Build-Prozess erforderliche Funktionalitäten (nämlich Hilfsprogramme zum Parsen von Objektdateien und Disassemblierung) enthält und zusätzliche Toolchains zusätzlichen Test- und Wartungsaufwand bedeuten, ist es praktisch, derzeit nur eine Hauptversion einer Toolchain zu unterstützen.

Bytecode des Basisinterpreters kompilieren

Die meisten früheren Implementierungen von Copy-and-Patch verwenden es als schnellen Baseline-JIT, während CPythons JIT die Technik verwendet, um optimierte Mikro-Op-Traces zu kompilieren.

In der Praxis liegt der neue JIT derzeit irgendwo zwischen den „Baseline“- und „Optimizing“-Compiler-Tiers anderer dynamischer Sprach-Runtimes. Dies liegt daran, dass CPython seinen spezialisierenden adaptiven Interpreter verwendet, um Laufzeit-Profiling-Informationen zu sammeln, die dann zur Erkennung und Optimierung von „Hot“-Pfaden im Code verwendet werden. Dieser Schritt wird mittels Self-Modifying Code durchgeführt, einer Technik, die mit einem JIT-Compiler schwieriger zu implementieren ist.

Obwohl es *möglich* ist, normalen Bytecode mit Copy-and-Patch zu kompilieren (tatsächlich gab es frühe Prototypen, die dem Mikro-Op-Interpreter vorausgingen und genau das taten), scheint dies einfach nicht genug Optimierungspotenzial zu bieten wie das granularere Mikro-Op-Format.

GPU-Unterstützung hinzufügen

Der JIT ist derzeit nur für CPUs. Er lädt beispielsweise keine NumPy-Array-Berechnungen auf CUDA-GPUs aus, wie es JITs wie Numba tun.

Es gibt bereits ein reiches Ökosystem von Werkzeugen zur Beschleunigung dieser Art von spezialisierten Aufgaben, und CPythons JIT ist nicht dazu gedacht, diese zu ersetzen. Stattdessen soll er die Leistung von Allzweck-Python-Code verbessern, der weniger wahrscheinlich von tieferer GPU-Integration profitiert.

Offene Fragen

Geschwindigkeit

Derzeit ist der JIT auf den meisten Plattformen etwa so schnell wie der bestehende spezialisierende Interpreter. Die Verbesserung dessen ist offensichtlich eine Top-Priorität zu diesem Zeitpunkt, da die Bereitstellung einer signifikanten Leistungssteigerung die gesamte Motivation für die Existenz eines JIT ist. Eine Reihe von vorgeschlagenen Verbesserungen ist bereits im Gange, und diese laufende Arbeit wird in GH-115802 verfolgt.

Speicher

Da er zusätzlichen Speicher für ausführbaren Maschinencode alloziert, verbraucht der JIT zur Laufzeit mehr Speicher als der bestehende Interpreter. Laut den offiziellen Benchmarks verbraucht der JIT derzeit etwa 10-20% mehr Speicher als der Basis-Interpreter. Das obere Ende dieser Spanne ist auf aarch64-apple-darwin zurückzuführen, das größere Seitengrößen (und damit eine größere minimale Allokationsgranularität) hat.

Diese Zahlen sollten jedoch mit Vorsicht genossen werden, da die Benchmarks selbst keinen sehr hohen Basis-Speicherverbrauch aufweisen. Da sie ein höheres Verhältnis von Code zu Daten haben, ist der Speicher-Overhead des JIT ausgeprägter als in einer typischen Arbeitslast, bei der der Speicherdruck wahrscheinlich ein echtes Problem darstellt.

Bisher wurde noch nicht viel Aufwand betrieben, um den Speicherverbrauch des JIT zu optimieren, daher stellen diese Zahlen wahrscheinlich ein Maximum dar, das im Laufe der Zeit reduziert wird. Die Verbesserung dessen ist eine mittlere Priorität und wird in GH-116017 verfolgt. Wir könnten in Zukunft erwägen, konfigurierbare Parameter zur Begrenzung des Speicherverbrauchs anzubieten, aber es werden keine offiziellen APIs bereitgestellt, bis der JIT die Anforderungen erfüllt, um als nicht-experimentell zu gelten.

Frühere Versionen des JIT hatten ein komplexeres Speicherallokationsschema, das eine Reihe fragiler Einschränkungen für die Größe und das Layout des ausgegebenen Codes auferlegte und den Speicher-Footprint ausführbarer Python-Dateien erheblich aufblähte. Diese Probleme sind im aktuellen Design nicht mehr vorhanden.

Abhängigkeiten

Zum Zeitpunkt des Schreibens hat der JIT eine Build-Zeit-Abhängigkeit von LLVM. LLVM wird verwendet, um einzelne Mikro-Op-Instruktionen in Maschinencode-Blobs zu kompilieren, die dann verlinkt werden, um die Templates des JIT zu bilden. Diese Templates werden verwendet, um CPython selbst zu bauen. Der JIT hat keine Laufzeitabhängigkeit von LLVM und ist daher für Endbenutzer keinerlei als Abhängigkeit sichtbar.

Das Bauen des JIT verlängert den Build-Prozess um 3 bis 60 Sekunden, je nach Plattform. Er wird nur dann neu kompiliert, wenn die generierten Dateien veraltet sind, sodass nur diejenigen, die aktiv an der Haupt-Interpreter-Schleife entwickeln, ihn mit einiger Häufigkeit neu bauen werden.

Im Gegensatz zu vielen anderen generierten Dateien in CPython werden die generierten Dateien des JIT nicht von Git verfolgt. Dies liegt daran, dass sie kompilierte Binärcode-Templates enthalten, die nicht nur für die Host-Plattform, sondern auch für die aktuelle Build-Konfiguration dieser Plattform spezifisch sind. Als solche würde ihr Hosting erhebliche Ingenieursarbeit erfordern, um Dutzende von großen Binärdateien für jeden Commit zu bauen und zu hosten, der den generierten Code ändert. Obwohl vielleicht machbar, ist dies keine Priorität, da die Installation der erforderlichen Werkzeuge für die meisten Leute, die CPython bauen, nicht unerschwinglich schwierig ist und der Build-Schritt nicht besonders zeitaufwendig ist.

Da einige immer noch an dieser Möglichkeit interessiert sind, wird die Diskussion in GH-115869 verfolgt.

Fußnoten


Quelle: https://github.com/python/peps/blob/main/peps/pep-0744.rst

Zuletzt geändert: 2025-02-01 07:28:42 GMT