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

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

Value Stack

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.