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 演讲:构建轻量级 IR 和后端用于 YJIT
-
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
通过在 Ruby 脚本中添加 puts RubyVM::YJIT.disasm(method(:method_name)),可以打印给定方法的机器码。请注意,如果方法未被编译,则不会生成代码。
命令行选项
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-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-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 hash 的类)
-
避免仅调用另一个方法的那些方法
-
Ruby 方法调用成本很高。避免诸如仅从 hash 返回值的这些方法
-
尝试编写代码,使相同的变量和方法参数始终具有相同的类型
-
避免使用
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 来报告错误或仅提出问题。非常欢迎有关如何使此 README 文件对新贡献者更有帮助的建议。
错误修复和错误报告对我们非常有价值。如果您在 YJIT 中发现错误,很可能之前没有人报告过,或者我们没有好的复现方法,所以请提交一个 issue,并提供有关您的配置以及您如何遇到问题的尽可能多的信息。列出您用于运行 YJIT 的命令,以便我们可以在我们的环境中轻松重现该问题并进行调查。如果您能生成一个小程序来重现该错误以帮助我们进行追踪,那将非常感激。
如果您想为 YJIT 贡献一个大型补丁,我们建议在 Shopify/ruby 存储库 上提交一个 issue 或发起讨论,以便我们能够进行积极的讨论。一个常见的问题是,有时人们会在事先未沟通的情况下向开源项目提交大型拉取请求,然后我们不得不拒绝它们,因为他们实现的工作不符合项目的设计。我们希望为您节省时间和减少挫败感,所以请与我们联系,以便我们能够就如何贡献我们将要合并到 YJIT 中的补丁进行富有成效的讨论。
源代码组织
YJIT 源代码分为
-
yjit.c:YJIT 用于与 CRuby 其余部分交互的代码
-
yjit.h:YJIT 向 CRuby 其余部分公开的 C 定义 -
yjit.rb:暴露给 Ruby 的
YJITRuby 模块 -
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-check运行快速检查以确保 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
出于开发目的,可以在 Apple M1 上通过 Rosetta 运行 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