YJIT - 又一個 Ruby JIT

YJIT 是一個輕量級、極簡主義的 Ruby JIT,建置在 CRuby 內部。它使用基本區塊版本化 (BBV) 架構,以惰性方式編譯程式碼。YJIT 目前支援在 x86-64 和 arm64/aarch64 CPU 上的 macOS、Linux 和 BSD。這個專案是開源的,且採用與 CRuby 相同的授權條款。

如果您在生產環境中使用 YJIT,請 與我們分享您的成功案例!

如果您想進一步瞭解所採取的方法,以下是一些會議演講和出版品:- RubyKaigi 2023 主題演講:從構思到生產,最佳化 YJIT 的效能 - RubyKaigi 2023 主題演講:將 Rust YJIT 融入 CRuby - RubyKaigi 2022 主題演講:開發 YJIT 的故事 - RubyKaigi 2022 演講:為 YJIT 建立輕量級 IR 和後端 - RubyKaigi 2021 演講:YJIT:在 CRuby 內建構新的 JIT 編譯器 - 部落格文章:YJIT:在 CRuby 內建構新的 JIT 編譯器 - MPLR 2023 論文:在生產環境中評估 YJIT 的效能:務實的方法 - VMIL 2021 論文:YJIT:CRuby 的基本區塊版本化 JIT 編譯器 - MoreVMs 2021 演講:YJIT:在 CRuby 內建構新的 JIT 編譯器 - ECOOP 2016 演講:JavaScript 程式的跨程序類型特化,無類型分析 - ECOOP 2016 論文:JavaScript 程式的跨程序類型特化,無類型分析 - ECOOP 2015 演講:透過惰性基本區塊版本化,進行簡單且有效的類型檢查移除 - ECOOP 2015 論文:透過惰性基本區塊版本化,進行簡單且有效的類型檢查移除

要在您的出版品中引用 YJIT,請引用 MPLR 2023 論文

@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}
}

目前的限制

YJIT 可能不適合某些應用程式。它目前僅支援 x86-64 和 arm64/aarch64 CPU 上的 macOS、Linux 和 BSD。YJIT 會比 Ruby 解譯器使用更多記憶體,因為 JIT 編譯器需要在記憶體中產生機器碼並維護其他狀態資訊。您可以使用 YJIT 的命令列選項 來變更分配多少可執行記憶體。

安裝

需求

您需要安裝:- C 編譯器,例如 GCC 或 Clang - GNU Make 和 Autoconf - Rust 編譯器 rustc 和 Cargo(如果您想要在開發/除錯模式中建置)- Rust 版本必須為 >= 1.58.0

若要安裝 Rust 建置工具鏈,我們建議遵循 建議的安裝方法。Rust 也為許多原始碼編輯器提供一流的 支援

建置 YJIT

首先複製 ruby/ruby 存放庫

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

YJIT ruby 二進位檔可以用 GCC 或 Clang 建置。它可以在開發(除錯)模式或發行模式中建置。若要獲得最佳效能,請使用 GCC 在發行模式中編譯 YJIT。更詳細的建置說明提供在 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

# 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

開發模式包含延伸的 YJIT 統計資料,但可能會很慢。您可以在統計資料模式中設定僅限統計資料

# 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

在 macOS 上,您可能需要指定尋找某些函式庫的位置

# 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

通常設定會選擇預設的 C 編譯器。若要指定 C 編譯器,請使用

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

在執行 ./configure 之前。

您可以透過執行來測試 YJIT 是否正確運作

# Quick tests found in /bootstraptest
make btest

# Complete set of tests
make -j test-all

用法

範例

建置好 YJIT 後,您可以從建置目錄中使用 ./miniruby,或使用 chruby 工具切換到 YJIT 版本的 ruby

chruby ruby-yjit
ruby myscript.rb

您可以透過使用 --yjit-stats 命令列選項來執行 YJIT,以傾印編譯和執行的統計資料

./miniruby --yjit-stats myscript.rb

針對特定方法產生的機器碼可以透過將 puts RubyVM::YJIT.disasm(method(:method_name)) 加入 Ruby 腳本來列印。請注意,如果方法未編譯,則不會產生任何程式碼。

命令列選項

YJIT 支援上游 CRuby 支援的所有命令列選項,但也會新增一些 YJIT 特定的選項

請注意,還有一個環境變數 RUBY_YJIT_ENABLE,可用於啟用 YJIT。這對於某些部署腳本很有用,在這些腳本中,為 Ruby 指定額外的命令列選項並不實際。

您也可以在執行時間使用 RubyVM::YJIT.enable 啟用 YJIT。這可以讓您在應用程式完成開機後啟用 YJIT,這可以避免編譯任何初始化程式碼。

您可以使用 RubyVM::YJIT.enabled? 或檢查 ruby --yjit -v 是否包含字串 +YJIT 來驗證 YJIT 是否已啟用

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

基準測試

我們收集了一組基準測試,並在 yjit-bench 儲存庫中實作了一個簡單的基準測試架構。此基準測試架構旨在停用 CPU 頻率調整、設定處理程序親和性,並停用地址空間隨機化,以便基準測試執行之間的差異盡可能小。

生產部署的效能提示

雖然 YJIT 選項預設為我們認為對大多數工作負載有用的設定,但它們不一定是最適合您應用程式的設定。本節涵蓋了在 YJIT 沒有加速生產環境中的應用程式時,改善 YJIT 效能的提示。

增加 –yjit-exec-mem-size

當 JIT 程式碼大小(RubyVM::YJIT.runtime_stats[:code_region_size])達到此值時,YJIT 會停止編譯新程式碼。增加可執行記憶體大小表示 YJIT 可以最佳化更多程式碼,但會以使用更多記憶體為代價。

如果您使用 --yjit-stats 啟動 Ruby,例如使用環境變數 RUBYOPT=--yjit-statsRubyVM::YJIT.runtime_stats[:ratio_in_yjit] 會以 % 顯示 YJIT 執行指令的比例。理想情況下,ratio_in_yjit 應盡可能大,達到 99%,而增加 --yjit-exec-mem-size 通常有助於改善 ratio_in_yjit

讓工作程序執行盡可能久

在程序重新啟動前,盡可能多次呼叫相同的程式碼會很有幫助。如果程序過於頻繁地被終止,編譯方法所需的時間可能會超過編譯它們所獲得的速度提升。

您應該監控每個程序已處理的請求數量。如果您定期終止工作程序,例如使用 unicorn-worker-killerpuma_worker_killer,您可能需要降低終止頻率或提高限制。

減少 YJIT 記憶體使用量

YJIT 為 JIT 程式碼和元資料分配記憶體。啟用 YJIT 通常會導致更多的記憶體使用量。本節將介紹在 YJIT 使用量超過您的容量時,最小化 YJIT 記憶體使用量的提示。

減少 –yjit-exec-mem-size

--yjit-exec-mem-size 選項指定 JIT 程式碼大小,但 YJIT 也會使用記憶體作為其元資料,而元資料通常會消耗比 JIT 程式碼更多的記憶體。一般而言,YJIT 會在 Ruby 3.3 的生產環境中增加大約 3-4 倍 --yjit-exec-mem-size 的記憶體開銷。您應該將這個數字乘以工作程序的數量,以估算最差情況下的記憶體開銷。

--yjit-exec-mem-size=48 是 Ruby 3.3.1 以來的預設值,但較小的值(例如 32 MiB)可能對您的應用程式更有意義。在這樣做的同時,您可能需要監控 RubyVM::YJIT.runtime_stats[:ratio_in_yjit],如上所述。

延遲啟用 YJIT

如果您透過 --yjit 選項或 RUBY_YJIT_ENABLE=1 啟用 YJIT,YJIT 可能會編譯僅在應用程式啟動期間使用的程式碼。RubyVM::YJIT.enable 允許您從 Ruby 程式碼中啟用 YJIT,您可以在應用程式初始化後呼叫它,例如在 Unicorn 的 after_fork 掛鉤中。如果您使用任何 YJIT 選項 (--yjit-*),YJIT 預設會在啟動時啟動,但 --yjit-disable 允許您在傳遞 YJIT 調整選項的同時,以 YJIT 已停用的模式啟動 Ruby。

程式碼最佳化提示

此部分包含有關撰寫 Ruby 程式碼的提示,這些程式碼將在 YJIT 上執行得盡可能快。其中一些建議基於 YJIT 的當前限制,而其他建議則廣泛適用。在程式碼庫中的每個地方套用這些提示可能不切實際。理想情況下,你應先使用 stackprof 等工具對應用程式進行剖析,以便確定哪些方法佔用大部分執行時間。然後,你可以重新整理構成執行時間中最大部分的特定方法。我們不建議根據 YJIT 的當前限制修改整個程式碼庫。

你也可以使用 --yjit-stats 命令列選項查看哪些位元組碼導致 YJIT 退出,並重新整理程式碼以避免在程式碼的最熱門方法中使用這些指令。

其他統計資料

如果你使用 --yjit-stats 執行 ruby,YJIT 會追蹤並在 RubyVM::YJIT.runtime_stats 中傳回效能統計資料。

$ 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,
...

一些計數器包括

以「exit_」開頭的計數器顯示 YJIT 程式碼採取側邊出口(返回直譯器)的原因。

效能計數器名稱無法保證在 Ruby 版本之間保持相同。如果您好奇每個計數器的含義,通常最好搜尋原始碼 — 但它可能會在後續的 Ruby 版本中變更。

--yjit-stats 執行之後列印的文字包含其他資訊,其名稱可能與 RubyVM::YJIT.runtime_stats 中的資訊不同。

貢獻

我們歡迎開源貢獻。您可以隨時開啟新的議題來回報錯誤或僅詢問問題。我們非常歡迎提供如何讓這個自述檔案對新貢獻者更有幫助的建議。

錯誤修正和錯誤回報對我們來說非常有價值。如果您在 YJIT 中發現錯誤,很有可能之前沒有人回報過,或者我們沒有良好的重現方式,所以請開啟一個議題並提供關於您的組態以及您如何遭遇問題的說明,並提供您能提供的資訊。列出您用來執行 YJIT 的指令,這樣我們就能輕鬆地重現問題並進行調查。如果您能產生一個重現錯誤的小程式來幫助我們追蹤問題,我們也會非常感謝。

如果您想為 YJIT 貢獻大型補丁,我們建議在 Shopify/ruby 儲存庫 上開啟議題或討論,以便我們可以進行積極的討論。一個常見的問題是,有時候人們會在沒有事先溝通的情況下向開源專案提交大型拉取請求,而我們必須拒絕這些請求,因為他們實作的工作不符合專案的設計。我們希望為您節省時間和挫折,因此請與我們聯繫,以便我們可以進行富有成效的討論,了解您如何貢獻我們想要合併到 YJIT 的補丁。

原始碼組織

YJIT 原始碼分為: - yjit.c:YJIT 用來與 CRuby 的其他部分介接的程式碼 - yjit.h:YJIT 提供給 CRuby 其他部分的 C 定義 - yjit.rb:提供給 Ruby 的 YJIT Ruby 模組 - yjit/src/asm/*:我們用來產生機器碼的記憶體組譯器 - yjit/src/codegen.rs:將 Ruby 位元組碼轉換為機器碼的邏輯 - yjit/src/core.rb:基本區塊版本控制邏輯,YJIT 的核心結構 - yjit/src/stats.rs:收集執行時間統計資料 - yjit/src/options.rs:處理命令列選項 - yjit/src/cruby.rs:手動提供給 Rust 程式碼庫的 C 繫結 - yjit/bindgen/src/main.rs:透過 bindgen 提供給 Rust 程式碼庫的 C 繫結

CRuby 的直譯器邏輯核心位於: - insns.def:定義 Ruby 的位元組碼指令(編譯到 vm.inc) - vm_insnshelper.c:Ruby 的位元組碼指令使用的邏輯 - vm_exec.c:Ruby 直譯器迴圈

使用 bindgen 產生 C 繫結

為了將 C 函式提供給 Rust 程式碼庫,您需要產生 C 繫結

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

這使用 bindgen 工具根據 yjit/bindgen/src/main.rs 中列出的繫結產生/更新 yjit/src/cruby_bindings.inc.rs。避免手動編輯這個檔案,因為它可能會在稍後自動重新產生。如果您需要手動新增 C 繫結,請將它們新增到 yjit/cruby.rs 中。

編碼和除錯秘訣

有多個測試套件: - make btest(請參閱 /bootstraptest) - make test-all - make test-spec - make check 執行以上所有測試 - make yjit-smoke-test 執行快速檢查以查看 YJIT 是否正常運作

測試可以像這樣並行執行

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

或者像這樣單執行緒執行,以便更容易找出哪個特定測試失敗

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

要在 test-all 中除錯單一測試

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

您也可以在 btest 中執行一個特定測試

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

有捷徑可以在 test.rb 中執行/偵錯您自己的測試/重製

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

您可以在 LLDB 中使用 Intel 語法進行反組譯,使其與 YJIT 的反組譯保持一致

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

在 Apple 的 Rosetta 上執行 x86 YJIT

出於開發目的,可以在 Apple M1 上通過 Rosetta 執行 x86 YJIT。您可以在下面找到基本說明,但有一些注意事項列在下方。

首先,安裝 Rosetta

$ softwareupdate --install-rosetta

現在,任何命令都可以通過 arch 命令列工具使用 Rosetta 執行。

然後,您可以在 x86 環境中啟動您的 shell

$ arch -x86_64 zsh

您可以通過 arch 命令再次檢查您的當前架構

$ arch -x86_64 zsh
$ arch
i386

您可能需要將 rustc 的預設目標設定為 x86-64,例如

$ rustup default stable-x86_64-apple-darwin

在您的 i386 shell 中,安裝 Cargo 和 Homebrew,然後開始編寫!

Rosetta 注意事項

  1. 您必須為每個架構安裝一個 Homebrew 版本

  2. Cargo 預設會安裝在 $HOME/.cargo 中,我不知道在安裝後如何更改架構

如果您使用 Fish shell,您可以 閱讀此連結,了解如何讓開發環境更輕鬆。

使用 Linux perf 分析

--yjit-perf 允許您使用 Linux perf 對 JIT 編譯的方法以及其他原生函式進行分析。當您使用 perf record 執行 Ruby 時,perf 會查詢 /tmp/perf-{pid}.map 以解析 JIT 程式碼中的符號,而此選項允許 YJIT 將方法符號寫入該檔案,並啟用框架指標。

以下是一個使用此選項與 Firefox Profiler 的範例方法(另請參閱:使用 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