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

可以使用 GCC 或 Clang 构建 YJIT ruby 二进制文件。它可以在开发(调试)模式或发布模式下构建。为了获得最佳性能,请使用 GCC 在发布模式下编译 YJIT。更详细的构建说明在Ruby 自述文件中提供。

# 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

通常,configure 会选择默认的 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

可以通过向 Ruby 脚本添加 puts RubyVM::YJIT.disasm(method(:method_name)) 来打印为给定方法生成的机器代码。请注意,如果方法未编译,则不会生成任何代码。

命令行选项

YJIT 支持上游 CRuby 支持的所有命令行选项,但也添加了一些 YJIT 特有的选项

请注意,还有一个环境变量 RUBY_YJIT_ENABLE,可用于启用 YJIT。对于某些部署脚本,在其中为 Ruby 指定额外的命令行选项不切实际,这可能很有用。

你还可以使用 RubyVM::YJIT.enable 在运行时启用 YJIT。这可以让你在应用程序启动完成后启用 YJIT,从而可以避免编译任何初始化代码。

你可以使用 RubyVM::YJIT.enabled? 验证 YJIT 是否已启用,或通过检查 ruby --yjit -v 是否包含字符串 +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-stats,则 RubyVM::YJIT.runtime_stats[:ratio_in_yjit] 显示 YJIT 执行的指令的比率,单位为 %。理想情况下,ratio_in_yjit 应尽可能大,达到 99%,并且增加 --yjit-exec-mem-size 通常有助于提高 ratio_in_yjit

尽可能长时间地运行工作进程

在进程重新启动之前尽可能多次调用相同的代码非常有用。如果进程被过频繁地终止,编译方法所花费的时间可能超过编译方法所获得的速度提升。

你应该监控每个进程已处理的请求数量。如果你定期终止工作进程,例如使用 `unicorn-worker-killer` 或 `puma_worker_killer`,你可能需要降低终止频率或增加限制。

减少 YJIT 内存使用

YJIT 为 JIT 代码和元数据分配内存。启用 YJIT 通常会导致更多的内存使用。本节将介绍在 YJIT 使用超过你的容量的情况下最小化 YJIT 内存使用的一些技巧。

减小 --yjit-exec-mem-size

`--yjit-exec-mem-size` 选项指定 JIT 代码大小,但 YJIT 也为其元数据使用内存,元数据通常消耗的内存比 JIT 代码更多。通常,自 Ruby 3.3 起,YJIT 在生产环境中增加的内存开销大约是 `--yjit-exec-mem-size` 的 3-4 倍。你应该将此值乘以工作进程的数量来估计最坏情况下的内存开销。

自 Ruby 3.3.1 起,`--yjit-exec-mem-size=48` 是默认值,但对于你的应用程序来说,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。

代码优化技巧

本节包含有关编写在 YJIT 上尽可能快运行的 Ruby 代码的技巧。其中一些建议基于 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 绑定

为了向 Rust 代码库公开 C 函数,您需要生成 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 中使用英特尔汇编语法,使其与 YJIT 的汇编保持一致

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

在 Apple 的 Rosetta 上运行 x86 YJIT

出于开发目的,可以通过 Rosetta 在 Apple M1 上运行 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