YJIT - Yet Another Ruby JIT

YJIT ist ein leichtgewichtiger, minimalistischer Ruby JIT, der in CRuby integriert ist. Er kompiliert Code verzögert (lazy) mittels einer Basic Block Versioning (BBV) Architektur. YJIT 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.

Wenn Sie YJIT in der Produktion verwenden, teilen Sie uns bitte Ihre Erfolgsgeschichten mit!

Wenn Sie mehr über den Ansatz erfahren möchten, finden Sie hier einige Konferenzvorträge und Veröffentlichungen

Um YJIT in Ihren Publikationen zu zitieren, zitieren Sie bitte das MPLR 2023 Papier

@inproceedings{yjit_mplr_2023,
author = {Chevalier-Boisvert, Maxime and Kokubun, Takashi and Gibbs, Noah and Wu, Si Xing (Alan) and Patterson, Aaron and Issroff, Jemma},
title = {Evaluating YJIT’s Performance in a Production Context: A Pragmatic Approach},
year = {2023},
isbn = {9798400703805},
publisher = {Association for Computing Machinery},
address = {New York, NY, USA},
url = {https://doi.org/10.1145/3617651.3622982},
doi = {10.1145/3617651.3622982},
booktitle = {Proceedings of the 20th ACM SIGPLAN International Conference on Managed Programming Languages and Runtimes},
pages = {20–33},
numpages = {14},
keywords = {dynamically typed, optimization, just-in-time, virtual machine, ruby, compiler, bytecode},
location = {Cascais, Portugal},
series = {MPLR 2023}
}

Aktuelle Einschränkungen

YJIT ist möglicherweise nicht für bestimmte Anwendungen geeignet. Es unterstützt derzeit nur macOS, Linux und BSD auf x86-64 und arm64/aarch64 CPUs. YJIT benötigt mehr Speicher als der Ruby-Interpreter, da der JIT-Compiler Maschinencode im Speicher generieren und zusätzliche Zustandsinformationen pflegen muss. Sie können die Menge des zugewiesenen ausführbaren Speichers über die Kommandozeilenoptionen von YJIT ändern.

Installation

Voraussetzungen

Sie müssen Folgendes installieren:

Wenn Sie keine Codeänderungen an YJIT selbst vornehmen möchten, empfehlen wir, rustc über den Paketmanager Ihres Betriebssystems zu beziehen, da dieser wahrscheinlich dieselben Zulieferer wiederverwendet, die auch die C-Toolchain bereitstellen.

Wenn Sie den Rust-Code von YJIT ändern werden, empfehlen wir die offizielle Installationsmethode für Rust. Rust bietet auch erstklassige Unterstützung für viele Quellcode-Editoren.

YJIT erstellen

Beginnen Sie mit dem Klonen des ruby/ruby Repositorys

git clone https://github.com/ruby/ruby yjit
cd yjit

Das YJIT ruby Binary kann entweder mit GCC oder Clang erstellt werden. Es kann entweder im Entwicklungs-(Debug-)Modus oder im Release-Modus erstellt werden. Für maximale Leistung kompilieren Sie YJIT im Release-Modus mit GCC. Detailliertere Bauanleitungen finden Sie in der Ruby README.

# Configure in release mode for maximum performance, build and install
./autogen.sh
./configure --enable-yjit --prefix=$HOME/.rubies/ruby-yjit --disable-install-doc
make -j && make install

oder

# Configure in lower-performance dev (debug) mode for development, build and install
./autogen.sh
./configure --enable-yjit=dev --prefix=$HOME/.rubies/ruby-yjit --disable-install-doc
make -j && make install

Der Entwicklungsmodus enthält erweiterte YJIT-Statistiken, kann aber langsam sein. Nur für Statistiken können Sie im Statistikmodus konfigurieren.

# Configure in extended-stats mode without slow runtime checks, build and install
./autogen.sh
./configure --enable-yjit=stats --prefix=$HOME/.rubies/ruby-yjit --disable-install-doc
make -j && make install

Unter macOS müssen Sie möglicherweise angeben, wo einige Bibliotheken zu finden sind.

# Install dependencies
brew install openssl libyaml

# Configure in dev (debug) mode for development, build and install
./autogen.sh
./configure --enable-yjit=dev --prefix=$HOME/.rubies/ruby-yjit --disable-install-doc --with-opt-dir="$(brew --prefix openssl):$(brew --prefix readline):$(brew --prefix libyaml)"
make -j && make install

Typischerweise wählt configure den Standard-C-Compiler. Um den C-Compiler anzugeben, verwenden Sie

# Choosing a specific c compiler
export CC=/path/to/my/chosen/c/compiler

bevor Sie ./configure ausführen.

Sie können testen, ob YJIT korrekt funktioniert, indem Sie ausführen:

# Quick tests found in /bootstraptest
make btest

# Complete set of tests
make -j test-all

Verwendung

Beispiele

Sobald YJIT erstellt ist, können Sie entweder ./miniruby aus Ihrem Build-Verzeichnis verwenden oder mit dem chruby-Tool zur YJIT-Version von ruby wechseln.

chruby ruby-yjit
ruby myscript.rb

Sie können Statistiken zur Kompilierung und Ausführung ausgeben, indem Sie YJIT mit der Kommandozeilenoption --yjit-stats ausführen.

./miniruby --yjit-stats myscript.rb

Sie können sehen, was YJIT kompiliert hat, indem Sie YJIT mit der Kommandozeilenoption --yjit-log ausführen.

./miniruby --yjit-log myscript.rb

Der für eine bestimmte Methode generierte Maschinencode kann durch Hinzufügen von puts RubyVM::YJIT.disasm(method(:method_name)) zu einem Ruby-Skript ausgegeben werden. Beachten Sie, dass kein Code generiert wird, wenn die Methode nicht kompiliert ist.

Kommandozeilenoptionen

YJIT unterstützt alle Kommandozeilenoptionen, die von Upstream CRuby unterstützt werden, fügt aber auch einige YJIT-spezifische Optionen hinzu.

Beachten Sie, dass es auch eine Umgebungsvariable RUBY_YJIT_ENABLE gibt, mit der YJIT aktiviert werden kann. Dies kann für einige Bereitstellungsskripte nützlich sein, bei denen die Angabe einer zusätzlichen Kommandozeilenoption für Ruby nicht praktikabel ist.

Sie können YJIT auch zur Laufzeit mit RubyVM::YJIT.enable aktivieren. Dies ermöglicht es Ihnen, YJIT zu aktivieren, nachdem Ihre Anwendung mit dem Booten fertig ist, was es ermöglicht, den Initialisierungscode nicht zu kompilieren.

Sie können überprüfen, ob YJIT aktiviert ist, indem Sie RubyVM::YJIT.enabled? verwenden oder indem Sie überprüfen, ob ruby --yjit -v die Zeichenkette +YJIT enthält.

ruby --yjit -v
ruby 3.3.0dev (2023-01-31T15:11:10Z master 2a0bf269c9) +YJIT dev [x86_64-darwin22]

ruby --yjit -e "p RubyVM::YJIT.enabled?"
true

ruby -e "RubyVM::YJIT.enable; p RubyVM::YJIT.enabled?"
true

Benchmarking

Wir haben eine Reihe von Benchmarks gesammelt und ein einfaches Benchmarking-Framework im Repository yjit-bench implementiert. Dieses Benchmarking-Framework ist darauf ausgelegt, die Skalierung der CPU-Frequenz zu deaktivieren, Prozessaffinitäten einzustellen und die zufällige Adressraumzuordnung zu deaktivieren, um die Varianz zwischen den Benchmark-Läufen so gering wie möglich zu halten.

Leistungstipps für Produktionsbereitstellungen

Obwohl die YJIT-Optionen standardmäßig so eingestellt sind, dass sie unserer Meinung nach für die meisten Workloads gut funktionieren, sind sie möglicherweise nicht die beste Konfiguration für Ihre Anwendung. Dieser Abschnitt enthält Tipps zur Verbesserung der YJIT-Leistung, falls YJIT Ihre Anwendung in der Produktion nicht beschleunigt.

Erhöhen von –yjit-mem-size

Der Wert --yjit-mem-size kann verwendet werden, um die maximale Speichermenge festzulegen, die YJIT verwenden darf. Dies entspricht der Summe von RubyVM::YJIT.runtime_stats[:code_region_size] und RubyVM::YJIT.runtime_stats[:yjit_alloc_size]. Das Erhöhen des Werts von --yjit-mem-size bedeutet, dass mehr Code von YJIT optimiert werden kann, auf Kosten einer höheren Speichernutzung.

Wenn Sie Ruby mit --yjit-stats starten, z. B. über die Umgebungsvariable RUBYOPT=--yjit-stats, zeigt RubyVM::YJIT.runtime_stats[:ratio_in_yjit] den Prozentsatz der von YJIT ausgeführten YARV-Instruktionen im Vergleich zum CRuby-Interpreter an. Idealerweise sollte ratio_in_yjit so hoch wie 99 % sein, und das Erhöhen von --yjit-mem-size hilft oft, ratio_in_yjit zu verbessern.

Worker so lange wie möglich laufen lassen

Es ist hilfreich, denselben Code so oft wie möglich aufzurufen, bevor ein Prozess neu gestartet wird. Wenn ein Prozess zu häufig beendet wird, können die für die Kompilierung von Methoden aufgewendeten Zeit die durch die Kompilierung erzielten Geschwindigkeitssteigerungen überwiegen.

Sie sollten die Anzahl der von jedem Prozess bedienten Anfragen überwachen. Wenn Sie Worker-Prozesse periodisch beenden, z. B. mit unicorn-worker-killer oder puma_worker_killer, sollten Sie die Häufigkeit des Beendens reduzieren oder das Limit erhöhen.

Reduzierung des YJIT-Speicherverbrauchs

YJIT reserviert Speicher für JIT-Code und Metadaten. Die Aktivierung von YJIT führt im Allgemeinen zu einem höheren Speicherverbrauch. Dieser Abschnitt enthält Tipps zur Minimierung des YJIT-Speicherverbrauchs, falls dieser Ihre Kapazität übersteigt.

Verringern von –yjit-mem-size

YJIT verwendet Speicher für kompilierten Code und Metadaten. Sie können die maximale Speichermenge, die YJIT verwenden kann, ändern, indem Sie eine andere Kommandozeilenoption --yjit-mem-size angeben. Der Standardwert ist derzeit 128. Wenn Sie diesen Wert ändern, sollten Sie RubyVM::YJIT.runtime_stats[:ratio_in_yjit] wie oben erklärt überwachen.

YJIT verzögert aktivieren

Wenn Sie YJIT über die Optionen --yjit oder RUBY_YJIT_ENABLE=1 aktivieren, kann YJIT Code kompilieren, der nur während des Bootens der Anwendung verwendet wird. RubyVM::YJIT.enable ermöglicht es Ihnen, YJIT aus Ruby-Code heraus zu aktivieren, und Sie können dies nach der Initialisierung Ihrer Anwendung aufrufen, z. B. im after_fork Hook von Unicorn. Wenn Sie YJIT-Optionen (--yjit-*) verwenden, startet YJIT standardmäßig beim Booten, aber --yjit-disable ermöglicht es Ihnen, Ruby im YJIT-deaktivierten Modus zu starten und gleichzeitig YJIT-Tuning-Optionen zu übergeben.

Codeoptimierungstipps

Dieser Abschnitt enthält Tipps zum Schreiben von Ruby-Code, der auf YJIT so schnell wie möglich ausgeführt wird. Einige dieser Ratschläge basieren auf aktuellen Einschränkungen von YJIT, während andere Ratschläge allgemein anwendbar sind. Es wird wahrscheinlich nicht praktikabel sein, diese Tipps überall in Ihrer Codebasis anzuwenden. Sie sollten idealerweise damit beginnen, Ihre Anwendung mit einem Tool wie stackprof zu profilieren, um festzustellen, welche Methoden den Großteil der Ausführungszeit ausmachen. Sie können dann die spezifischen Methoden refaktorieren, die die größten Anteile der Ausführungszeit ausmachen. Wir empfehlen nicht, Ihre gesamte Codebasis basierend auf den aktuellen Einschränkungen von YJIT zu ändern.

Sie können auch die Kommandozeilenoption --yjit-stats verwenden, um zu sehen, welche Bytecodes YJIT zum Beenden veranlassen, und Ihren Code refaktorieren, um die Verwendung dieser Anweisungen in den heißesten Methoden Ihres Codes zu vermeiden.

Andere Statistiken

Wenn Sie ruby mit --yjit-stats ausführen, verfolgt und gibt YJIT Leistungsinformationen in RubyVM::YJIT.runtime_stats zurück.

$ RUBYOPT="--yjit-stats" irb
irb(main):001:0> RubyVM::YJIT.runtime_stats
=>
{:inline_code_size=>340745,
 :outlined_code_size=>297664,
 :all_stats=>true,
 :yjit_insns_count=>1547816,
 :send_callsite_not_simple=>7267,
 :send_kw_splat=>7,
 :send_ivar_set_method=>72,
...

Einige der Zähler umfassen:

Zähler, die mit "exit_" beginnen, zeigen die Gründe für einen Side-Exit von YJIT-Code (Rücksprung zum Interpreter) an.

Namen von Leistungsparametern sind nicht garantiert, dass sie zwischen Ruby-Versionen gleich bleiben. Wenn Sie neugierig sind, was jeder Zähler bedeutet, ist es am besten, den Quellcode danach zu durchsuchen – aber er kann sich in einer späteren Ruby-Version ändern.

Der nach einem --yjit-stats-Lauf ausgegebene Text enthält weitere Informationen, die anders benannt sein können als die Informationen in RubyVM::YJIT.runtime_stats.

Mitwirkung

Wir begrüßen Open-Source-Beiträge. Sie können gerne neue Issues eröffnen, um Fehler zu melden oder einfach Fragen zu stellen. Vorschläge zur Verbesserung dieser Readme-Datei für neue Mitwirkende sind sehr willkommen.

Fehlerbehebungen und Fehlerberichte sind für uns sehr wertvoll. Wenn Sie einen Fehler in YJIT finden, ist es sehr wahrscheinlich, dass niemand ihn zuvor gemeldet hat oder dass wir keine gute Reproduktionsmöglichkeit dafür haben. Bitte eröffnen Sie ein Issue 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, die Sie zum Ausführen von YJIT verwendet haben, damit wir das Problem leicht reproduzieren und untersuchen können. Wenn Sie ein kleines Programm erstellen können, das den Fehler reproduziert, um uns bei der Nachverfolgung zu helfen, ist das sehr willkommen.

Wenn Sie einen großen Patch zu YJIT beitragen möchten, empfehlen wir, ein Issue oder eine Diskussion im Repository Shopify/ruby zu eröffnen, damit wir eine aktive Diskussion führen können. Ein häufiges Problem ist, dass manchmal Leute 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, also nehmen Sie bitte Kontakt auf, damit wir eine produktive Diskussion darüber führen können, wie Sie Patches beisteuern können, die wir in YJIT aufnehmen möchten.

Organisation des Quellcodes

Der Quellcode von YJIT ist aufgeteilt in:

Der Kern der Interpreterlogik von CRuby befindet sich in:

Generierung von C-Bindungen mit Bindgen

Um C-Funktionen für die Rust-Codebasis verfügbar zu machen, müssen Sie C-Bindungen generieren.

CC=clang ./configure --enable-yjit=dev
make -j yjit-bindgen

Dies verwendet die Bindgen-Tools, um yjit/src/cruby_bindings.inc.rs basierend auf den in yjit/bindgen/src/main.rs aufgeführten Bindungen zu generieren/aktualisieren. Vermeiden Sie die manuelle Bearbeitung dieser Datei, da sie zu einem späteren Zeitpunkt automatisch neu generiert werden könnte. Wenn Sie manuell C-Bindungen hinzufügen müssen, fügen Sie sie stattdessen zu yjit/cruby.rs hinzu.

Coding & Debugging Protips

Es gibt mehrere Test-Suiten:

Die Tests können parallel wie folgt ausgeführt werden:

make -j test-all RUN_OPTS="--yjit-call-threshold=1"

Oder ein-threaded wie folgt, um leichter zu identifizieren, welcher spezifische Test fehlschlägt:

make test-all TESTOPTS=--verbose RUN_OPTS="--yjit-call-threshold=1"

Um eine einzelne Testdatei mit test-all auszuführen:

make test-all TESTS='test/-ext-/marshal/test_usrmarshal.rb' RUNRUBYOPT=--debugger=lldb RUN_OPTS="--yjit-call-threshold=1"

Es ist auch möglich, Tests nach Namen zu filtern, um einen einzelnen Test auszuführen:

make test-all TESTS='-n /test_float_plus/' RUN_OPTS="--yjit-call-threshold=1"

Sie können auch einen bestimmten Test in btest ausführen:

make btest BTESTS=bootstraptest/test_ractor.rb RUN_OPTS="--yjit-call-threshold=1"

Es gibt Verknüpfungen zum Ausführen/Debuggen Ihres eigenen Tests/Reproduktionsfalls in test.rb.

make run  # runs ./miniruby test.rb
make lldb # launches ./miniruby test.rb in lldb

Sie können die Intel-Syntax für die Disassemblierung in LLDB verwenden, um sie mit der Disassemblierung von YJIT konsistent zu halten.

echo "settings set target.x86-disassembly-flavor intel" >> ~/.lldbinit

x86 YJIT auf Apples Rosetta ausführen

Zu Entwicklungszwecken ist es möglich, x86 YJIT auf einem Apple M1 über Rosetta auszuführen. Grundlegende Anleitungen finden Sie unten, aber es gibt einige Einschränkungen, die weiter unten aufgeführt sind.

Installieren Sie zuerst Rosetta.

$ softwareupdate --install-rosetta

Jetzt kann jeder Befehl über das Kommandozeilen-Tool arch mit Rosetta ausgeführt werden.

Starten Sie dann Ihre Shell in einer x86-Umgebung.

$ arch -x86_64 zsh

Sie können Ihre aktuelle Architektur mit dem Befehl arch überprüfen.

$ arch -x86_64 zsh
$ arch
i386

Möglicherweise müssen Sie das Standardziel für rustc auf x86-64 setzen, z. B.

$ rustup default stable-x86_64-apple-darwin

Installieren Sie währenddessen in Ihrer i386-Shell Cargo und Homebrew, und legen Sie dann los!

Rosetta-Einschränkungen

  1. Sie müssen eine Version von Homebrew für jede Architektur installieren.

  2. Cargo wird standardmäßig unter $HOME/.cargo installiert, und ich kenne keine gute Möglichkeit, die Architektur nach der Installation zu ändern.

Wenn Sie die Fish-Shell verwenden, können Sie diesen Link lesen, um Informationen zu erhalten, wie Sie die Entwicklungsumgebung einfacher gestalten können.

Profiling mit Linux perf

--yjit-perf ermöglicht Ihnen, JIT-kompilierte Methoden zusammen mit anderen nativen Funktionen mit Linux perf zu profilieren. Wenn Sie Ruby mit perf record ausführen, sucht perf nach /tmp/perf-{pid}.map, um Symbole im JIT-Code aufzulösen, und diese Option ermöglicht es YJIT, Methodensymbole in diese Datei zu schreiben und Frame-Pointer zu aktivieren.

Aufrufgraph

Hier ist ein Beispiel, wie diese Option mit Firefox Profiler verwendet werden kann (siehe auch: Profiling mit Linux perf).

# Compile the interpreter with frame pointers enabled
./configure --enable-yjit --prefix=$HOME/.rubies/ruby-yjit --disable-install-doc cflags=-fno-omit-frame-pointer
make -j && make install

# [Optional] Allow running perf without sudo
echo 0 | sudo tee /proc/sys/kernel/kptr_restrict
echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid

# Profile Ruby with --yjit-perf
cd ../yjit-bench
PERF="record --call-graph fp" ruby --yjit-perf -Iharness-perf benchmarks/liquid-render/benchmark.rb

# View results on Firefox Profiler https://profiler.firefox.com.
# Create /tmp/test.perf as below and upload it using "Load a profile from file".
perf script --fields +pid > /tmp/test.perf

YJIT Codegen

Sie können auch die Anzahl der Zyklen profilieren, die für den von jeder YJIT-Funktion generierten Code verbraucht werden.

# Install perf
apt-get install linux-tools-common linux-tools-generic linux-tools-`uname -r`

# [Optional] Allow running perf without sudo
echo 0 | sudo tee /proc/sys/kernel/kptr_restrict
echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid

# Profile Ruby with --yjit-perf=codegen
cd ../yjit-bench
PERF=record ruby --yjit-perf=codegen -Iharness-perf benchmarks/lobsters/benchmark.rb

# Aggregate results
perf script > /tmp/perf.txt
../ruby/misc/yjit_perf.py /tmp/perf.txt

Perf mit Python-Unterstützung erstellen

Die obigen Anweisungen funktionieren für die meisten Leute gut, aber Sie könnten auch eine praktische perf script -s-Schnittstelle verwenden, wenn Sie perf aus dem Quellcode erstellen.

# Build perf from source for Python support
sudo apt-get install libpython3-dev python3-pip flex libtraceevent-dev \
  libelf-dev libunwind-dev libaudit-dev libslang2-dev libdw-dev
git clone --depth=1 https://github.com/torvalds/linux
cd linux/tools/perf
make
make install

# Aggregate results
perf script -s ../ruby/misc/yjit_perf.py