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 testcargo 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-testtest/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.本机堆栈

2. Ruby VM 堆栈

Ruby VM 使用一个单一的连续内存区域 (ec->vm_stack),其中包含两个向彼此增长的子堆栈。当它们相遇时,就会发生堆栈溢出。

有关详细的架构和帧布局,请参阅 doc/contributing/vm_stack_and_frames.md

控制帧堆栈

值堆栈

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 当物理寄存器不足时,将寄存器值移至内存的过程。