ZJIT: ADVANCED RUBY JIT PROTOTYP
ZJIT ist ein methodenbasierter Just-in-Time (JIT) Compiler für Ruby. Er nutzt Profiling-Informationen aus dem Interpreter, um Optimierungen im Compiler zu steuern.
ZJIT wird derzeit für macOS, Linux und BSD auf x86-64 und arm64/aarch64 CPUs unterstützt. Dieses Projekt ist Open Source und unterliegt derselben Lizenz wie CRuby.
Aktuelle Einschränkungen
ZJIT ist möglicherweise nicht für bestimmte Anwendungen geeignet. Derzeit werden nur macOS, Linux und BSD auf x86-64 und arm64/aarch64 CPUs unterstützt. ZJIT verbraucht mehr Speicher als der Ruby-Interpreter, da der JIT-Compiler Maschinencode im Speicher generieren und zusätzliche Zustandsinformationen verwalten muss. Sie können die Menge des zugewiesenen ausführbaren Speichers mit den Kommandozeilenoptionen von ZJIT ändern.
Mitwirkung
Wir freuen uns über Open-Source-Beiträge. Zögern Sie nicht, neue Issues zu eröffnen, um Fehler zu melden oder Fragen zu stellen. Vorschläge, wie dieses Dokument für neue Mitwirkende hilfreicher gestaltet werden kann, sind sehr willkommen.
Fehlerbehebungen und Fehlerberichte sind für uns sehr wertvoll. Wenn Sie einen Fehler in ZJIT finden, ist es gut möglich, dass er noch niemandem gemeldet wurde oder wir keine gute Reproduktionsmöglichkeit dafür haben. Bitte eröffnen Sie daher ein Ticket auf dem offiziellen Ruby-Bugtracker (oder, wenn Sie kein Konto erstellen möchten, auf Shopify/ruby) und geben Sie so viele Informationen wie möglich über Ihre Konfiguration und eine Beschreibung an, wie Sie auf das Problem gestoßen sind. Listen Sie die Befehle auf, mit denen Sie ZJIT ausgeführt haben, damit wir das Problem leicht auf unserer Seite reproduzieren und untersuchen können. Wenn Sie ein kleines Programm erstellen können, das den Fehler reproduziert, um uns bei der Fehlersuche zu helfen, wird dies ebenfalls sehr geschätzt.
Wenn Sie einen größeren Patch zu ZJIT beitragen möchten, schlagen wir vor, sich auf Zulip zu unterhalten und dann ein Issue im Shopify/ruby-Repository zu eröffnen, damit wir eine technische Diskussion führen können. Ein häufiges Problem ist, dass Leute manchmal große Pull-Requests an Open-Source-Projekte senden, ohne vorherige Kommunikation, und wir müssen sie ablehnen, weil die implementierte Arbeit nicht zum Design des Projekts passt. Wir möchten Ihnen Zeit und Frustration ersparen, daher nehmen Sie bitte Kontakt auf, damit wir eine produktive Diskussion darüber führen können, wie Sie Patches beitragen können, die wir in ZJIT integrieren möchten.
Build-Anweisungen
Siehe Building Ruby für allgemeine Build-Voraussetzungen. Zusätzlich benötigt ZJIT Rust 1.85.0 oder neuer. Release-Builds benötigen nur rustc. Development-Builds benötigen cargo und können Abhängigkeiten herunterladen. GNU Make wird benötigt.
Für normale Nutzung
ZJIT unter macOS bauen
./autogen.sh
./configure \
--enable-zjit \
--prefix="$HOME"/.rubies/ruby-zjit \
--disable-install-doc \
--with-opt-dir="$(brew --prefix openssl):$(brew --prefix readline):$(brew --prefix libyaml)"
make -j miniruby
ZJIT unter Linux bauen
./autogen.sh
./configure \
--enable-zjit \
--prefix="$HOME"/.rubies/ruby-zjit \
--disable-install-doc
make -j miniruby
Für die Entwicklung
ZJIT unter macOS bauen
./autogen.sh
./configure \
--enable-zjit=dev \
--prefix="$HOME"/.rubies/ruby-zjit \
--disable-install-doc \
--with-opt-dir="$(brew --prefix openssl):$(brew --prefix readline):$(brew --prefix libyaml)"
make -j miniruby
ZJIT unter Linux bauen
./autogen.sh
./configure \
--enable-zjit=dev \
--prefix="$HOME"/.rubies/ruby-zjit \
--disable-install-doc
make -j miniruby
Beachten Sie, dass --enable-zjit=dev viele IR-Validierungen durchführt, was hilft, Fehler frühzeitig zu erkennen, aber die Kompilierung und das Aufwärmen erheblich verlangsamt.
Die gültigen Werte für --enable-zjit sind, von schnellster zu langsamster: * --enable-zjit: ZJIT im Release-Modus für maximale Leistung aktivieren * --enable-zjit=stats: ZJIT im erweiterten Statistikmodus aktivieren * --enable-zjit=dev_nodebug: ZJIT im Entwicklungsmodus aktivieren, aber ohne langsame Laufzeitprüfungen * --enable-zjit=dev: ZJIT im Debug-Modus für die Entwicklung aktivieren, aktiviert auch RUBY_DEBUG
Bindings neu generieren
Beim Modifizieren von zjit/bindgen/src/main.rs müssen Sie die Bindings in zjit/src/cruby_bindings.inc.rs neu generieren mit
make zjit-bindgen
Dokumentation
Kommandozeilenoptionen
Siehe ruby --help für ZJIT-spezifische Kommandozeilenoptionen
$ ruby --help
...
ZJIT options:
--zjit-mem-size=num
Max amount of memory that ZJIT can use in MiB (default: 128).
--zjit-call-threshold=num
Number of calls to trigger JIT (default: 30).
--zjit-num-profiles=num
Number of profiled calls before JIT (default: 5).
--zjit-stats[=quiet]
Enable collecting ZJIT statistics (=quiet to suppress output).
--zjit-disable Disable ZJIT for lazily enabling it with RubyVM::ZJIT.enable.
--zjit-perf Dump ISEQ symbols into /tmp/perf-{}.map for Linux perf.
--zjit-log-compiled-iseqs=path
Log compiled ISEQs to the file. The file will be truncated.
--zjit-trace-exits[=counter]
Record source on side-exit. `Counter` picks specific counter.
--zjit-trace-exits-sample-rate=num
Frequency at which to record side exits. Must be `usize`.
$
Source-Level-Dokumentation
Sie können die Source-Level-Dokumentation generieren und in Ihrem Browser öffnen mit
cargo doc --document-private-items -p zjit --open
Graph des Typsystems
Sie können einen Graphen der ZJIT-Typenhierarchie generieren mit
ruby zjit/src/hir_type/gen_hir_type.rb > zjit/src/hir_type/hir_type.inc.rs dot -O -Tpdf zjit_types.dot open zjit_types.dot.pdf
Tests
Beachten Sie, dass Tests gegen CRuby gelinkt werden. Direkte Aufrufe von cargo test oder cargo nextest sollten daher nicht funktionieren. Alle Tests werden stattdessen über make aufgerufen.
Einrichtung
Stellen Sie zunächst sicher, dass Sie cargo installiert haben. Wenn nicht, können Sie es über rustup.rs installieren.
Installieren Sie auch cargo-binstall mit
cargo install cargo-binstall
Stellen Sie sicher, dass Sie --enable-zjit=dev angeben, wenn Sie configure ausführen, und installieren Sie dann die folgenden Tools
cargo binstall --secure cargo-nextest cargo binstall --secure cargo-insta
cargo-insta wird für die Aktualisierung von Snapshots verwendet. cargo-nextest führt jeden Test in einem eigenen Prozess aus, was wichtig ist, da CRuby nur einmal pro Prozess gebootet werden kann und die meisten APIs nicht Thread-sicher sind.
Unit-Tests ausführen
Für das Testen der Funktionalität innerhalb von ZJIT verwenden Sie
make zjit-test
Sie können auch einen einzelnen Testfall ausführen, indem Sie den Funktionsnamen angeben
make zjit-test ZJIT_TESTS=test_putobject
Snapshot-Tests
ZJIT verwendet insta für Snapshot-Tests innerhalb von Unit-Tests. Wenn Tests aufgrund von Snapshot-Abweichungen fehlschlagen, werden ausstehende Snapshots erstellt. Der Testbefehl benachrichtigt Sie, wenn ausstehende Snapshots vorhanden sind.
Pending snapshots found. Accept with: make zjit-test-update
Alle Snapshot-Änderungen aktualisieren/akzeptieren
make zjit-test-update
Sie können Snapshot-Änderungen auch interaktiv einzeln überprüfen
cd zjit && cargo insta review
Teständerungen werden zusammen mit Code-Änderungen überprüft.
Integrationstests ausführen
Dieser Befehl führt Ruby-Ausführungstests aus.
make test-all TESTS="test/ruby/test_zjit.rb"
Sie können auch einen einzelnen Testfall ausführen, indem Sie den Methodennamen abgleichen
make test-all TESTS="test/ruby/test_zjit.rb -n TestZJIT#test_putobject"
Alle Tests ausführen
Führt sowohl make zjit-test als auch test/ruby/test_zjit.rb aus
make zjit-check
Statistiksammlung
ZJIT bietet detaillierte Statistiken über das Verhalten von JIT-Kompilierung und -Ausführung.
Basisstatistiken
Ausführen mit Basisstatistiken, die beim Beenden ausgegeben werden
./miniruby --zjit-stats script.rb
Sammeln von Statistiken ohne Ausgabe (Zugriff über RubyVM::ZJIT.stats in Ruby)
./miniruby --zjit-stats=quiet script.rb
Zugriff auf Statistiken in Ruby
# Check if stats are enabled if RubyVM::ZJIT.stats_enabled? stats = RubyVM::ZJIT.stats puts "Compiled ISEQs: #{stats[:compiled_iseq_count]}" puts "Failed ISEQs: #{stats[:failed_iseq_count]}" # You can also reset stats during execution RubyVM::ZJIT.reset_stats! end
Performance-Verhältnis
Die Statistik ratio_in_zjit zeigt den Prozentsatz der Ruby-Instruktionen an, die im JIT-Code im Vergleich zum Interpreter ausgeführt wurden. Diese Metrik erscheint nur, wenn ZJIT mit --enable-zjit=stats oder höher (was die Verfolgung von rb_vm_insn_count aktiviert) kompiliert wurde, und stellt einen wichtigen Leistungskennwert für die Effektivität von ZJIT dar.
Verfolgung von Side Exits
Über Stackprof können detaillierte Informationen über die Methoden angezeigt werden, aus denen der JIT ausscheidet (side-exits). Optional können Sie --zjit-trace-exits-sample-rate=N verwenden, um jedes N-te Vorkommen zu sampeln. Die Aktivierung von --zjit-trace-exits-sample-rate=N aktiviert automatisch --zjit-trace-exits.
./miniruby --zjit-trace-exits script.rb
Eine Datei namens zjit_exits_{pid}.dump wird im selben Verzeichnis wie script.rb erstellt. Das Anzeigen der Side-Exit-Methoden kann mit Stackprof erfolgen.
stackprof path/to/zjit_exits_{pid}.dump
HIR in Iongraph anzeigen
Die Verwendung von --zjit-dump-hir-iongraph gibt alle kompilierten Funktionen in ein Verzeichnis namens /tmp/zjit-iongraph-{PROCESS_PID} aus. Jede Datei wird func_{ZJIT_FUNC_NAME}.json genannt. Um sie im Iongraph-Viewer zu verwenden, müssen Sie jq verwenden, um sie zu einer einzigen Datei zusammenzufassen. Eine Beispielaufrufung von jq ist unten zur Referenz aufgeführt.
jq --slurp --null-input '.functions=inputs | .version=1' /tmp/zjit-iongraph-{PROCESS_PID}/func*.json > ~/Downloads/ion.json
Von dort aus können Sie mozilla-spidermonkey.github.io/iongraph/ verwenden, um Ihre Trace anzuzeigen.
ZJIT-Fehler ausgeben
--zjit-debug gibt ZJIT-Kompilierungsfehler und andere Diagnosen aus
./miniruby --zjit-debug script.rb
Wie der Name schon sagt, ist diese Option hauptsächlich für ZJIT-Entwickler gedacht.
Nützliche Entwicklerbefehle
Um YARV-Ausgabe für Codeausschnitte anzuzeigen
./miniruby --dump=insns -e0
Codeausschnitte mit ZJIT ausführen
./miniruby --zjit -e0
Sie können auch www.rubyexplorer.xyz/ ausprobieren, um Ruby YARV Disasm-Ausgaben mit Syntaxhervorhebung anzuzeigen, die leicht mit anderen Teammitgliedern geteilt werden können.
Ruby-Stacks verstehen
Die Ruby-Ausführung umfasst drei verschiedene Stacks, und das Verständnis dieser wird Ihnen helfen, die Implementierung von ZJIT zu verstehen.
1. Nativer Stack
-
Zweck: Rücksprungadressen und gespeicherte Register. ZJIT verwendet ihn auch für Argument-Arrays einiger C-Funktionen.
-
Verwaltung: Betriebssystem-verwaltet, einer pro nativem Thread
-
Wachstum: Abwärts von hohen Adressen
-
Konstanten:
NATIVE_STACK_PTR,NATIVE_BASE_PTR
2. Ruby VM Stack
Die Ruby VM verwendet einen einzelnen zusammenhängenden Speicherbereich (ec->vm_stack), der zwei Teil-Stacks enthält, die aufeinander zu wachsen. Wenn sie aufeinandertreffen, tritt ein Stack-Überlauf auf.
Siehe doc/contributing/vm_stack_and_frames.md für detaillierte Architektur und Frame-Layout.
Control Frame Stack
-
Speichert: Frame-Metadaten (
rb_control_frame_t-Strukturen) -
Wachstum: Abwärts von
vm_stack + size(hohe Adressen) -
Konstanten:
CFP
Value Stack
-
Speichert: YARV-Bytecode-Operanden (self, Argumente, lokale Variablen, temporäre Variablen)
-
Wachstum: Aufwärts von
vm_stack(niedrige Adressen) -
Konstanten:
SP
ZJIT Glossar
Dieses Glossar enthält Begriffe, die für das Verständnis von ZJIT hilfreich sind.
Bitte beachten Sie, dass einige Begriffe auch in CRuby-Internals vorkommen können, aber mit anderer Bedeutung.
| Begriff | Definition |
|---|---|
| HIR | High-level Intermediate Representation. Hochstufige (Ruby-Semantik) Graphendarstellung in Static Single Assignment (SSA)-Form. |
| LIR | Low-level Intermediate Representation. Niedrigstufige IR, die im Backend für die Assembler-Generierung verwendet wird. |
| SSA | Static Single Assignment. Eine Form, bei der jede Variable genau einmal zugewiesen wird. |
opnd |
Operand. Ein Operand einer IR-Instruktion (kann Register, Speicher, Immediate usw. sein). |
dst |
Ziel. Der Ausgabeoperand einer Instruktion, in dem das Ergebnis gespeichert wird. |
| VReg | Virtuelles Register. Ein virtuelles Register, das zu einem physischen Register oder Speicher abgebildet wird. |
insn_id |
Instruktions-ID. Ein Index einer Instruktion in einer Funktion. |
block_id |
Der Index eines grundlegenden Blocks, der effektiv wie ein Zeiger wirkt. |
branch |
Kontrollflusskante zwischen grundlegenden Blöcken im kompilierten Code. |
cb |
Code Block. Speicherbereich für generierten Maschinencode. |
entry |
Die Startadresse des kompilierten Codes für ein ISEQ. |
| Patch Point | Ort im generierten Code, der später modifiziert werden kann, falls Annahmen ungültig werden. |
| Frame State | Erfasster Zustand des Ruby-Stack-Frames an einem bestimmten Punkt für die Deoptimierung. |
| Guard | Eine Laufzeitprüfung, die sicherstellt, dass Annahmen weiterhin gültig sind. |
invariant |
Eine Annahme, auf der JIT-Code basiert und die eine Invalidierung erfordert, wenn sie gebrochen wird. |
| Deopt | Deoptimierung. Prozess des Zurückfallens von JIT-Code zum Interpreter. |
| Side Exit | Austritt aus JIT-Code zurück zum Interpreter. |
| Type Lattice | Hierarchie von Typen, die für Typinferenz und Optimierung verwendet wird. |
| Constant Folding | Optimierung, die konstante Ausdrücke zur Kompilierzeit auswertet. |
| RSP | x86-64 Stack-Pointer-Register, das für native Stack-Operationen verwendet wird. |
| Register Spilling | Prozess des Verschiebens von Registerwerten in den Speicher, wenn keine physischen Register mehr verfügbar sind. |