YJIT - 又一个 Ruby JIT¶ ↑
YJIT 是一个轻量级的、最小化的 Ruby JIT,构建在 CRuby 内部。它使用基本块版本控制 (BBV) 架构来延迟编译代码。YJIT 目前支持 macOS、Linux 和 BSD 上的 x86-64 和 arm64/aarch64 CPU。此项目是开源的,并与 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 可能不适用于某些应用程序。它目前仅支持 macOS、Linux 和 BSD 上的 x86-64 和 arm64/aarch64 CPU。YJIT 将比 Ruby 解释器使用更多内存,因为 JIT 编译器需要在内存中生成机器代码并维护额外的状态信息。您可以使用 YJIT 的命令行选项来更改分配多少可执行内存。
安装¶ ↑
要求¶ ↑
您需要安装
如果您不打算对 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 内存使用量的技巧。
减少 –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 来报告 bug 或只是提问。非常欢迎有关如何使此自述文件对新贡献者更有帮助的建议。
Bug 修复和 bug 报告对我们来说非常宝贵。如果您在 YJIT 中发现 bug,很可能之前没有人报告过它,或者我们没有很好的重现方法,因此请打开一个 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 绑定¶ ↑
为了向 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"
还可以按名称过滤测试以运行单个测试
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,然后开始 hack!
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