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 特有的选项
-
--yjit
:启用 YJIT(默认禁用) -
--yjit-exec-mem-size=N
:要分配的可执行内存块的大小,单位为 MiB(默认 48 MiB) -
--yjit-call-threshold=N
:在 YJIT 开始编译函数之前调用的次数。其默认值为 30,然后当进程中的 ISEQ 数量达到 40,000 时增加到 120。 -
--yjit-cold-threshold=N
:全局调用次数,在此之后 ISEQ 被视为冷调用且不会被编译,较低的值意味着编译的代码更少(默认 200K) -
--yjit-stats
:在程序执行后打印统计信息(会产生运行时成本) -
--yjit-stats=quiet
:在运行程序时收集统计信息,但不要打印它们。可以通过RubyVM::YJIT.runtime_stats
访问统计信息。(会产生运行时成本) -
--yjit-disable
:禁用 YJIT,尽管其他--yjit*
标志可以延迟启用它,方法是RubyVM::YJIT.enable
-
--yjit-code-gc
:启用代码GC
(从 Ruby 3.3 开始默认禁用)。当可执行内存大小限制达到时,它将导致丢弃所有机器代码,这意味着 JIT 编译将重新开始。这可以让你使用较低的可执行内存大小限制,但当达到限制时可能会导致性能略微下降。 -
--yjit-perf
:使用perf
工具启用帧指针和分析 -
--yjit-trace-exits
:生成特定退出点的回溯Marshal
转储。自动启用--yjit-stats
-
--yjit-trace-exits-sample-rate=N
:仅在每 N 次出现时跟踪退出位置
请注意,还有一个环境变量 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 的当前限制修改你的整个代码库。
-
避免使用 `OpenStruct`
-
避免重新定义基本整数运算(即 +, -, <, > 等)
-
避免重新定义 `nil`、相等性等的含义
-
避免在代码的热点部分分配对象
-
最大程度减少间接层
-
如果可以,避免使用包装对象的类
-
避免仅调用另一个方法的方法,即简单的单行方法
-
尝试编写代码,以便相同的变量始终具有相同的类型
-
如果可以,请使用
while
循环,而不是Array#each
等 C 方法 -
这不是惯用的 Ruby,但可能有助于热点方法
-
CRuby 方法调用代价高昂。避免诸如仅从哈希返回一个值或返回一个常量的方法。
您还可以使用 --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, ...
一些计数器包括
-
:yjit_insns_count - 已执行的 Ruby 字节码指令数
-
:binding_allocations - 分配的绑定数
-
:binding_set - 通过绑定设置的变量数
-
:code_gc_count - 自进程启动以来已编译代码的垃圾回收次数
-
:vm_insns_count - Ruby 解释器执行的指令数
-
:compiled_iseq_count - 已编译的字节码序列数
-
:inline_code_size - 已编译 YJIT 块的字节大小
-
:outline_code_size - YJIT 错误处理已编译代码的字节大小
-
:side_exit_count - 运行时采取的旁路出口数
-
:total_exit_count - 运行时采取的出口数,包括旁路出口
-
:avg_len_in_yjit - 退出到解释器之前已编译块中的平均指令数
以“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 注意事项¶ ↑
-
您必须为每个架构安装一个版本的 Homebrew
-
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