YJIT - 又一个 Ruby JIT¶ ↑
YJIT 是在 CRuby 内部构建的轻量级、极简的 Ruby JIT。它使用基本块版本控制(BBV)架构延迟编译代码。目前,YJIT 在 x86-64 和 arm64/aarch64 CPU 上的 macOS、Linux 和 BSD 上受支持。该项目是开源的,并遵循与 CRuby 相同的许可证。
如果您在生产环境中使用 YJIT,请与我们分享您的成功故事!
如果您想了解更多关于所采用方法的信息,以下是一些会议演讲和出版物
-
MPLR 2023 演讲:在生产环境中评估 YJIT 的性能:一种务实的方法
-
RubyKaigi 2023 主题演讲:优化 YJIT 的性能,从初始到生产
-
RubyKaigi 2023 主题演讲:将 Rust YJIT 融入 CRuby
-
RubyKaigi 2022 主题演讲:开发 YJIT 的故事
-
RubyKaigi 2022 演讲:为 YJIT 构建轻量级 IR 和后端
-
RubyKaigi 2021 演讲: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 的命令行选项更改分配的执行内存量。
安装¶ ↑
要求¶ ↑
您需要安装
-
所有用于 Ruby 的常用构建工具。请参阅 构建 Ruby
-
Rust 编译器
rustc
-
Rust 版本必须 >= 1.58.0。
-
-
可选,仅当您希望在开发/调试模式下构建时,才需要 Rust 的
cargo
如果您不打算更改 YJIT 本身的代码,我们建议通过操作系统的软件包管理器获取 rustc
,因为它可能会重用提供 C 工具链的同一供应商。
如果您要更改 YJIT 的 Rust 代码,我们建议使用 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
通常,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
您可以使用 --yjit-log
命令行选项运行 YJIT 来查看 YJIT 编译的内容
./miniruby --yjit-log myscript.rb
可以通过将 puts RubyVM::YJIT.disasm(method(:method_name))
添加到 Ruby 脚本来打印为给定方法生成的机器代码。 请注意,如果该方法未编译,则不会生成任何代码。
命令行选项
YJIT 支持上游 CRuby 支持的所有命令行选项,但也添加了一些 YJIT 特定的选项
-
--yjit
:启用 YJIT(默认禁用) -
--yjit-mem-size=N
:YJIT 内存使用量的软限制,单位为 MiB(默认:128)。尝试限制code_region_size + yjit_alloc_size
-
--yjit-exec-mem-size=N
:可执行内存块的硬限制,单位为 MiB。限制code_region_size
-
--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-log[=file|dir]
:将所有编译事件记录到指定的文件或目录。如果未提供名称,则当应用程序退出时,最后 1024 个日志条目将打印到 stderr。 -
--yjit-log=quiet
:收集最近 YJIT 编译的循环缓冲区。可以通过RubyVM::YJIT.log
访问编译日志条目,如果未快速清空缓冲区,则旧条目将被丢弃。(会产生运行时成本) -
--yjit-disable
:禁用 YJIT,尽管有其他--yjit*
标志,但可以使用RubyVM::YJIT.enable
延迟启用 YJIT -
--yjit-code-gc
:启用代码GC
(在 Ruby 3.3 中默认禁用)。当达到可执行内存大小限制时,它将导致所有机器代码被丢弃,这意味着 JIT 编译将重新开始。这可以允许您使用较低的可执行内存大小限制,但在达到限制时可能会导致性能略有下降。 -
--yjit-perf
:启用帧指针,并使用perf
工具进行性能分析 -
--yjit-trace-exits
:生成所有退出的回溯的Marshal
转储。自动启用--yjit-stats
-
--yjit-trace-exits=COUNTER
:生成计数退出或回退的回溯的Marshal
转储。自动启用--yjit-stats
-
--yjit-trace-exits-sample-rate=N
:仅每 N 次出现跟踪退出位置。自动启用--yjit-trace-exits
请注意,还有一个环境变量 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-mem-size¶ ↑
可以使用 --yjit-mem-size
值来设置 YJIT 允许使用的最大内存量。这对应于 RubyVM::YJIT.runtime_stats[:code_region_size]
和 RubyVM::YJIT.runtime_stats[:yjit_alloc_size]
的总和。增加 --yjit-mem-size
值意味着 YJIT 可以优化更多的代码,但代价是会占用更多的内存。
如果使用 --yjit-stats
启动 Ruby,例如使用环境变量 RUBYOPT=--yjit-stats
,RubyVM::YJIT.runtime_stats[:ratio_in_yjit]
会显示由 YJIT 执行的总 YARV 指令百分比,而不是由 CRuby 解释器执行的。理想情况下,ratio_in_yjit
应该高达 99%,而增加 --yjit-mem-size
通常有助于提高 ratio_in_yjit
。
尽可能长时间地运行工作进程¶ ↑
在进程重启之前,尽可能多次调用相同的代码是有帮助的。如果进程被频繁杀死,则编译方法所花费的时间可能会超过通过编译它们获得的速度提升。
你应该监控每个进程已处理的请求数量。如果你定期杀死工作进程,例如使用 unicorn-worker-killer
或 puma_worker_killer
,你可能需要降低杀死频率或增加限制。
减少 YJIT 内存使用量¶ ↑
YJIT 为 JIT 代码和元数据分配内存。启用 YJIT 通常会导致更多的内存使用。本节介绍在 YJIT 使用超过你的容量时,如何最小化 YJIT 内存使用量。
减少 –yjit-mem-size¶ ↑
YJIT 使用内存来存储编译后的代码和元数据。你可以通过指定不同的 --yjit-mem-size
命令行选项来更改 YJIT 可以使用的最大内存量。默认值目前为 128
。在更改此值时,你可能需要监控 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
、相等性等的含义 -
避免在代码的热点部分分配对象
-
尽量减少间接层
-
如果可以,避免编写包装器类(例如,仅包装 Ruby 哈希的类)
-
避免只调用另一个方法的方法
-
Ruby 方法调用是昂贵的。避免诸如仅从哈希返回值的方法
-
尝试编写代码,以便相同的变量和方法参数始终具有相同的类型
-
避免使用
TracePoint
,因为它会导致 YJIT 反优化代码 -
避免使用
binding
,因为它会导致 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, ...
一些计数器包括
-
: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
中的信息名称不同。
贡献¶ ↑
我们欢迎开源贡献。你可以随意打开新的 issue 来报告错误或只是提出问题。欢迎提出关于如何使此自述文件对新贡献者更有帮助的建议。
错误修复和错误报告对我们非常有价值。如果你在 YJIT 中发现错误,很可能以前没有人报告过,或者我们没有很好的重现方式,所以请打开一个 issue 并尽可能多地提供有关你的配置的信息以及你如何遇到问题的描述。列出你用来运行 YJIT 的命令,以便我们可以在我们的端轻松重现问题并进行调查。如果你能够生成一个小程序来重现错误以帮助我们跟踪它,我们也表示非常感谢。
如果你想为 YJIT 贡献一个大的补丁,我们建议在 Shopify/ruby 存储库 上打开一个 issue 或讨论,以便我们可以进行积极的讨论。一个常见的问题是,有时人们在没有事先沟通的情况下向开源项目提交大型 pull request,我们必须拒绝它们,因为他们实现的工作不符合项目的设计。我们想为你节省时间和挫败感,所以请联系我们,以便我们可以就如何贡献我们将希望合并到 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"
也可以按名称过滤测试以运行单个测试
make test-all TESTS='-n /test_float_plus/' 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¶ ↑
为了开发目的,可以通过 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
YJIT 代码生成¶ ↑
您还可以分析每个 YJIT 函数生成的代码所消耗的周期数。
# Install perf apt-get install linux-tools-common linux-tools-generic linux-tools-`uname -r` # [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=codegen cd ../yjit-bench PERF=record ruby --yjit-perf=codegen -Iharness-perf benchmarks/lobsters/benchmark.rb # Aggregate results perf script > /tmp/perf.txt ../ruby/misc/yjit_perf.py /tmp/perf.txt
构建支持 Python 的 perf¶ ↑
以上说明对大多数人来说都适用,但是如果您从源代码构建 perf,您也可以使用方便的 perf script -s
接口。
# Build perf from source for Python support sudo apt-get install libpython3-dev python3-pip flex libtraceevent-dev \ libelf-dev libunwind-dev libaudit-dev libslang2-dev libdw-dev git clone --depth=1 https://github.com/torvalds/linux cd linux/tools/perf make make install # Aggregate results perf script -s ../ruby/misc/yjit_perf.py