ZJIT:高级 Ruby JIT 原型
ZJIT 是一个基于方法的 Ruby 即时 (JIT) 编译器。它使用解释器的配置文件信息来指导编译器的优化。
ZJIT 目前支持 x86-64 和 arm64/aarch64 CPU 上的 macOS、Linux 和 BSD。该项目是开源的,遵循与 CRuby 相同的许可证。
当前限制
ZJIT 可能不适用于某些应用程序。它目前仅支持 x86-64 和 arm64/aarch64 CPU 上的 macOS、Linux 和 BSD。ZJIT 将比 Ruby 解释器占用更多内存,因为 JIT 编译器需要在内存中生成机器代码并维护其他状态信息。您可以使用 ZJIT 的命令行选项来更改分配的可执行内存量。
贡献
我们欢迎开源贡献。您可以随时提出新的问题来报告错误或只是提问。如果您对如何使这份文档对新贡献者更有帮助,我们也非常欢迎。
错误修复和错误报告对我们非常有价值。如果您在 ZJIT 中发现错误,很可能之前没有人报告过,或者我们没有好的复现方法,所以请在 官方 Ruby 错误跟踪器(或者,如果您不想注册账户,可以到 Shopify/ruby)上提交一个工单,并尽可能提供您的配置信息以及遇到问题的描述。列出您用来运行 ZJIT 的命令,以便我们能够轻松地在我们的端上复现问题并进行调查。如果您能够提供一个小程序来复现错误,以便我们追踪,我们将不胜感激。
如果您想为 ZJIT 贡献一个大型补丁,我们建议您先在 Zulip 上随意交流,然后再向 Shopify/ruby 仓库提交一个问题,以便我们可以进行技术讨论。一个常见的问题是,有时人们会在事先没有沟通的情况下向开源项目提交大型拉取请求,而我们不得不拒绝它们,因为他们实现的工作不符合项目的设计。我们希望为您节省时间和减少挫败感,所以请联系我们,以便我们进行富有成效的讨论,说明您如何能够贡献我们愿意合并到 ZJIT 中的补丁。
构建说明
有关一般的构建先决条件,请参阅 Building Ruby。此外,ZJIT 需要 Rust 1.85.0 或更高版本。发布构建只需要 rustc。开发构建需要 cargo,并可能下载依赖项。需要 GNU Make。
常规使用
在 macOS 上构建 ZJIT
./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
在 Linux 上构建 ZJIT
./autogen.sh
./configure \
--enable-zjit \
--prefix="$HOME"/.rubies/ruby-zjit \
--disable-install-doc
make -j miniruby
开发用
在 macOS 上构建 ZJIT
./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
在 Linux 上构建 ZJIT
./autogen.sh
./configure \
--enable-zjit=dev \
--prefix="$HOME"/.rubies/ruby-zjit \
--disable-install-doc
make -j miniruby
请注意,--enable-zjit=dev 会进行大量的 IR 验证,这有助于及早发现错误,但意味着编译和预热会明显变慢。
--enable-zjit 的有效值,从最快到最慢依次是:* --enable-zjit:以发布模式启用 ZJIT 以获得最佳性能 * --enable-zjit=stats:以扩展统计模式启用 ZJIT * --enable-zjit=dev_nodebug:以开发模式启用 ZJIT,但没有慢速运行时检查 * --enable-zjit=dev:以调试模式启用 ZJIT 以进行开发,同时启用 RUBY_DEBUG
重新生成绑定
修改 zjit/bindgen/src/main.rs 时,需要使用以下命令在 zjit/src/cruby_bindings.inc.rs 中重新生成绑定:
make zjit-bindgen
文档
命令行选项
有关 ZJIT 特定的命令行选项,请参阅 ruby --help。
$ 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`.
$
源代码文档
您可以使用以下命令生成并用浏览器打开源代码文档:
cargo doc --document-private-items -p zjit --open
类型系统图
您可以使用以下命令生成 ZJIT 类型层次结构的图:
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
测试
请注意,测试链接到 CRuby,因此直接调用 cargo test 或 cargo nextest 不应进行构建。所有测试都通过 make 访问。
设置
首先,请确保您已安装 cargo。如果尚未安装,可以使用 rustup.rs。
另外,使用以下命令安装 cargo-binstall:
cargo install cargo-binstall
在运行 configure 时,请务必添加 --enable-zjit=dev,然后安装以下工具:
cargo binstall --secure cargo-nextest cargo binstall --secure cargo-insta
cargo-insta 用于更新快照。cargo-nextest 在其自己的进程中运行每个测试,这很有价值,因为 CRuby 只支持每个进程启动一次,并且大多数 API 不是线程安全的。
运行单元测试
要测试 ZJIT 内部的功能,请使用:
make zjit-test
您也可以通过指定函数名来运行单个测试用例:
make zjit-test ZJIT_TESTS=test_putobject
快照测试
ZJIT 在单元测试中使用 insta 进行快照测试。当测试因快照不匹配而失败时,会创建待处理快照。测试命令会通知您是否有待处理快照。
Pending snapshots found. Accept with: make zjit-test-update
更新/接受所有快照更改:
make zjit-test-update
您也可以逐一交互式地查看快照更改:
cd zjit && cargo insta review
测试更改将与代码更改一起进行审查。
运行集成测试
此命令运行 Ruby 执行测试。
make test-all TESTS="test/ruby/test_zjit.rb"
您也可以通过匹配方法名来运行单个测试用例:
make test-all TESTS="test/ruby/test_zjit.rb -n TestZJIT#test_putobject"
运行所有测试
运行 make zjit-test 和 test/ruby/test_zjit.rb。
make zjit-check
统计信息收集
ZJIT 提供有关 JIT 编译和执行行为的详细统计信息。
基本统计信息
运行并在退出时打印基本统计信息:
./miniruby --zjit-stats script.rb
收集统计信息而不打印(在 Ruby 中通过 RubyVM::ZJIT.stats 访问):
./miniruby --zjit-stats=quiet script.rb
在 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
性能比
ratio_in_zjit 统计数据显示在 JIT 代码中执行的 Ruby 指令占总执行指令的百分比。此指标仅在 ZJIT 使用 --enable-zjit=stats 或更高级别(启用 rb_vm_insn_count 跟踪)构建时出现,并且是 ZJIT 有效性的关键性能指标。
追踪侧退出
通过 Stackprof,在程序执行一段时间后,可以显示有关 JIT 发生侧退出的方法的详细信息。您还可以选择使用 --zjit-trace-exits-sample-rate=N 来对每 N 次出现进行采样。启用 --zjit-trace-exits-sample-rate=N 将自动启用 --zjit-trace-exits。
./miniruby --zjit-trace-exits script.rb
一个名为 zjit_exits_{pid}.dump 的文件将被创建在 script.rb 所在的目录中。可以使用 Stackprof 查看侧退出方法:
stackprof path/to/zjit_exits_{pid}.dump
在 Iongraph 中查看 HIR
使用 --zjit-dump-hir-iongraph 将把所有编译的函数转储到一个名为 /tmp/zjit-iongraph-{PROCESS_PID} 的目录中。每个文件将被命名为 func_{ZJIT_FUNC_NAME}.json。为了在 Iongraph 查看器中使用它们,您需要使用 jq 将它们合并到一个文件中。下面提供了一个 jq 的示例调用作为参考。
jq --slurp --null-input '.functions=inputs | .version=1' /tmp/zjit-iongraph-{PROCESS_PID}/func*.json > ~/Downloads/ion.json
从那里,您可以使用 mozilla-spidermonkey.github.io/iongraph/ 来查看您的跟踪。
打印 ZJIT 错误
--zjit-debug 打印 ZJIT 编译错误和其他诊断信息。
./miniruby --zjit-debug script.rb
正如您从名称中可以猜到的,此选项主要用于 ZJIT 开发人员。
有用的开发命令
查看代码片段的 YARV 输出:
./miniruby --dump=insns -e0
使用 ZJIT 运行代码片段:
./miniruby --zjit -e0
您还可以尝试 www.rubyexplorer.xyz/ 来以易于与团队成员共享的方式查看具有语法高亮显示的 Ruby YARV 反汇编输出。
理解 Ruby 堆栈
Ruby 执行涉及三个不同的堆栈,理解它们将帮助您理解 ZJIT 的实现。
1.本机堆栈
-
目的:返回地址和保存的寄存器。ZJIT 还将其用于某些 C 函数的参数数组。
-
管理:操作系统管理,每个本机线程一个。
-
增长:从高地址向下增长。
-
常量:
NATIVE_STACK_PTR、NATIVE_BASE_PTR。
2. Ruby VM 堆栈
Ruby VM 使用一个单一的连续内存区域 (ec->vm_stack),其中包含两个向彼此增长的子堆栈。当它们相遇时,就会发生堆栈溢出。
有关详细的架构和帧布局,请参阅 doc/contributing/vm_stack_and_frames.md。
控制帧堆栈
-
存储:帧元数据 (
rb_control_frame_t结构)。 -
增长:从
vm_stack + size(高地址) 向下增长。 -
常量:
CFP。
值堆栈
-
存储:YARV 字节码操作数 (self、参数、局部变量、临时变量)。
-
增长:从
vm_stack(低地址) 向上增长。 -
常量:
SP。
ZJIT 词汇表
此词汇表包含有助于理解 ZJIT 的术语。
请注意,某些术语也可能出现在 CRuby 内部,但含义不同。
| 术语 | 定义 |
|---|---|
| HIR | 高级中间表示。高级(Ruby 语义)的静态单赋值 (SSA) 形式的图表示。 |
| LIR | 低级中间表示。后端用于生成汇编的低级 IR。 |
| SSA | 静态单赋值。一种变量被赋值一次的形式。 |
opnd |
操作数。IR 指令的操作数(可以是寄存器、内存、立即数等)。 |
dst |
目标。指令的结果存储在其上的输出操作数。 |
| VReg | 虚拟寄存器。将被降低到物理寄存器或内存的虚拟寄存器。 |
insn_id |
指令 ID。函数中指令的索引。 |
block_id |
基本块的索引,它有效地充当指针。 |
branch |
编译代码中基本块之间的控制流边。 |
cb |
代码块。生成机器代码的内存区域。 |
entry |
ISEQ 编译代码的起始地址。 |
| Patch Point | 生成代码中可以在以后修改的位置,以防假设失效。 |
| Frame State | 在特定点捕获的 Ruby 堆栈帧状态,用于反优化。 |
| Guard | 运行时检查,确保假设仍然有效。 |
invariant |
JIT 代码依赖的假设,如果被打破则需要失效。 |
| Deopt | 反优化。从 JIT 代码回退到解释器的过程。 |
| Side Exit | 从 JIT 代码退出到解释器。 |
| Type Lattice | 用于类型推断和优化的类型层次结构。 |
| Constant Folding | 在编译时评估常量表达式的优化。 |
| RSP | x86-64 堆栈指针寄存器,用于本机堆栈操作。 |
| Register Spilling | 当物理寄存器不足时,将寄存器值移至内存的过程。 |