- Published on
ZJIT 的 Lightweight Frames — Ruby 4.1 如何清除方法帧压栈,及其代价
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言 — Ruby 4.0 上线了 ZJIT,却没有打开它
- 为什么还要再造一个编译器 — YJIT 与 ZJIT 的设计差异
- 仍然存在的瓶颈 — 方法帧压栈
- Lightweight Frames — 只写一个指针,其余延后
- 这个想法在 2023 年已经失败过一次
- 实际数字 — 第一个 PR 几乎没有变快
- 接下来的四个月 — 栈溢出(spill)、内联器、版本数上限
- 诚实的现状 — ZJIT 仍不是默认选项
- 什么时候不该用
- 这个故事与并行无关
- 结语
- 参考资料
引言 — Ruby 4.0 上线了 ZJIT,却没有打开它
Ruby 4.0.0 于 2025 年 12 月 25 日发布。版本号从 3.5 跳到 4.0 这件事本身就是新闻,但在运行时这一侧,最引人注目的是 ZJIT。照搬官方发布说明的说法 — ZJIT 是作为 YJIT 下一代而开发的新 JIT 编译器,构建它需要 Rust 1.85.0 以上版本,而且必须指定 --zjit 才会启用。
紧接着,同一段落里还有这样一句话。
ZJIT is faster than the interpreter, but not yet as fast as YJIT. We encourage you to experiment with ZJIT, but maybe hold off on deploying it in production for now. Stay tuned for Ruby 4.1 ZJIT.
一份发布说明,一边把新编译器塞进正式发布里,一边又写「暂时先别把它部署到生产环境」,这种坦率并不多见。这句话诚实到这个地步,而与此同时下一句已经是预告了 — 敬请期待 Ruby 4.1 的 ZJIT。
今天是 2026 年 7 月 16 日。距离 4.0 发布还差一点不到七个月,4.1 还没有发布。如果逐个提交地追踪这段时间里瞄准 4.1 的 ZJIT 工作到底合并了什么,故事的中心会落在一样东西上 —— Lightweight Frames。本文写的就是这个东西。
为什么还要再造一个编译器 — YJIT 与 ZJIT 的设计差异
先说背景。YJIT 已经运转得很好,Shopify 在内的不少地方都把它用在生产环境。既然如此,同一个团队为什么还要再造一个编译器?ZJIT 的发布文章给出了两个理由。
第一,为了抬高性能天花板。 YJIT 基于 LBBV(lazy basic block versioning),把小块的基本块一片一片地编译。这种方式预热快、实现也稳健,但编译单元一小,编译器能看到的上下文也就跟着小。ZJIT 反其道而行之:把整个方法(或更大的单元)提升为 SSA 形式的高层 IR(HIR)再编译。单元一旦变大,常量折叠、分支折叠、类型推断这类传统优化才真正开始起作用。
第二,为了吸引外部贡献。 用发布文章的话说,「成为一个更传统的方法编译器」能降低贡献的门槛。LBBV 固然优雅,但它不是编译器教科书里会出现的东西。SSA 和 linear scan 则会出现。
第二个理由不是场面话。看下面会提到的这些 PR 就知道 — Lightweight Frames 和栈溢出(spill)来自 k0kubun(Shopify),方法内联器和版本数上限来自 nirvdrum,块内联来自 luke-gruber。发布文章末尾附的贡献者名单是用 git log --pretty="%an" zjit | sort -u 拉出来的,超过 30 人。
仍然存在的瓶颈 — 方法帧压栈
如果只能挑一个理由说明 Ruby 为什么慢,那就是方法调用的开销。YJIT 通过为多种调用形态特化机器码,把这部分成本削掉了不少。但 k0kubun 在 RubyKaigi 2026(2026 年 4 月 22–24 日,函馆)的演讲摘要说,还剩一样东西 —— 压栈 Ruby 方法帧的过程。这场演讲的题目是「Lightning-Fast Method Calls with Ruby 4.1 ZJIT」,摘要里写着,会在 4.1 上讨论「一个足以让人从 YJIT 换到 ZJIT 的有说服力的理由」。
要看清楚帧压栈为什么昂贵,看解释器每次调用都写了什么就够了。它会压入一个 ec->cfp,并往这个新帧里填入 cfp->pc(程序计数器)、cfp->iseq(是哪条指令序列)、cfp->block_code、cfp->sp,以及局部变量和栈槽。JIT 生成的机器码在大多数情况下只写这些值,从不读它们。 会去读的是解释器、异常处理、回溯、调试器。
也就是说,每次调用都老老实实填好的元数据,大部分是几乎没人会去读的保险。发布文章的「To do」条目就是这么写这个问题的。
Right now we have a lot of traffic to the VM frame. JIT frame pushes are reasonably fast, but with every effectful operation, we have to flush our local variable state and stack state to the VM frame. The instances in which code might want to read this reified frame state are rare: frame unwinding due to exceptions,
Binding#local_variable_get, etc. In the future, we will instead defer writing this state until it needs to be read.
Lightweight Frames 就是那个「in the future」。
Lightweight Frames — 只写一个指针,其余延后
设计文档是公开的。Shopify/ruby#909,k0kubun 在 2025 年 12 月 10 日开的 issue。核心的一句话就在第一行。
When ZJIT pushes an interpreter frame, it should write only one metadata pointer, and others should be lazily materialized.
具体运作是这样的。
- 压栈帧。
ec->cfp照常压入。但写进帧里的只有一样东西 —— 把持有返回地址的原生栈槽的地址,写进cfp->jit_return。在 x86_64 上,那就是call指令压入的那个栈槽的地址;在 arm64 上,对于 JIT-to-JIT 调用,用的是被调用方的帧建立过程保存链接寄存器那个槽的地址。 - 恢复(materialize)。 如果
cfp->jit_return非零,就以这个返回地址为键,去查一张「返回地址到元数据」的哈希表,取出编译期就造好的元数据。元数据里有 PC、ISEQ、栈大小、cme、env 标志、self 的位置,以及 specval 的类型和位置。里面还带着到帧基址指针的偏移量,靠它可以从原生栈反推出栈槽和局部变量。 - 什么时候恢复。 设计文档列出了三种情况 —— 异常抛出、longjmp 卷走 JIT 帧、由解释器接手执行时(OSR);需要回溯时(
Kernel#caller、使用rb_profile_frames的性能分析器);以及通过rb_debug_inspectorAPI 动态访问某个 JIT 帧的 Binding 时。
已合并的 PR(ruby/ruby#16262,2026 年 2 月 27 日创建,3 月 27 日合并,34 个文件 +800/-192)覆盖的是其中前半部分。用 PR 描述的话说,就是在 JIT-to-JIT 调用中去掉对 cfp->pc、cfp->iseq,以及大部分 cfp->block_code 的写入,改写 cfp->jit_return。
再确认一次消失的是什么 —— 每次调用原本写三次,现在写一次。就这么多。而这「就这么多」为什么会这么难,是下一节的事。
这个想法在 2023 年已经失败过一次
设计文档里最有价值的部分不是设计本身,而是 Prior art(前人工作) 这一节。同一个团队之前试过同样的想法,失败了。
Frame outlining(2023 年)。 Alan Wu 和 k0kubun 试验过的方案。它把一个带标记的指针塞进 cfp->pc,用来标示「这个帧被外提了」,只要标记存在,就用这个指针指向的元数据把帧恢复出来。问题在于代价。设计文档的原话是这样的。
Every read of
cfp->pc,cfp->sp, orcfp->iseqhad a branch on whethercfp->pcis tagged or not. If it's a tagged pointer, it points to frame metadata to materialize the outlined frame. Because we made every read ofpc/sp/iseqslower, the interpreter became slower. So we gave it up.
想让 JIT 变快,结果却让解释器变慢了。在 Ruby 里,这近乎判了死刑 —— 一项让没打开 JIT 的代码变慢的优化是不能被接受的。
Lightweight Frames 正是为了避开这个陷阱而设计的。区别在于乐观程度。在大多数 cfp 读取处,cfp->jit_return 的检查会被乐观地跳过(只在调试模式下加断言),只有在上面列出的那三种「必须恢复」的情形里才会检查。也就是说,检查的成本被推到了罕见路径上,而不是常见路径。
Lazy frame push。 另一项前人工作是 ruby/ruby#10080(「YJIT: Lazily push a frame for specialized C funcs」,k0kubun,2024 年 2 月 23 日合并)。这一个成功了,进了 YJIT,直到今天还在 master 里。但思路不同 —— 它压根不压栈帧(不压 ec->cfp),只有等被调用方真要抛异常时才压。Lightweight Frames 是会压 ec->cfp 的,所以设计文档的预期是,它能省掉 lazy frame push 原本要付出的那份检查开销。
这里该学到的不是技术,而是方法论。这个团队清楚地知道三年前那个想法的死因,并据此把新设计画成能避开那个死因的样子。所以 PR #16262 的基准测试表里,排在第一位的是解释器性能。因为那正是 2023 年那次死掉的地方。
实际数字 — 第一个 PR 几乎没有变快
从这里开始,本文要说最实在的话了。PR #16262 的描述里,作者本人是这么写的。
As of this PR, we don't expect a speedup in ZJIT on most benchmarks because the increased overhead of exits outweighs the benefit.
「就这个 PR 而言,我们并不期待大多数基准测试会有速度提升。」这是作者在提交一个 800 行 PR 时亲手写下的一句话。
来看数字。以下全部是 k0kubun 本人测量的数值,条件是 ruby-bench、x86_64-linux、ruby 4.1.0dev。列的含义是 before/after,所以大于 1 就是改善。
先看解释器。这正是 2023 年那次死掉的地方。
-------------- ------------- ------------- ------------
bench before (ms) after (ms) before/after
activerecord 140.8 ± 1.4% 138.8 ± 1.0% 1.015
chunky-png 408.4 ± 0.5% 402.4 ± 0.8% 1.015
hexapdf 1419.4 ± 0.8% 1392.1 ± 2.9% 1.020
liquid-c 32.9 ± 1.9% 33.5 ± 4.0% 0.982
railsbench 1331.8 ± 0.4% 1316.8 ± 0.5% 1.011
-------------- ------------- ------------- ------------
用 PR 的话说,「没有观察到对解释器性能有显著影响」。全部 15 个基准测试的比值都落在 0.982 到 1.022 之间,和误差范围重叠。这不是改善的证据,而是没有回归的证据,而这正是这张表存在的目的。
接下来是 ZJIT。
-------------- ------------- ------------- ------------
bench before (ms) after (ms) before/after
chunky-png 242.0 ± 0.8% 237.3 ± 0.7% 1.020
psych-load 821.7 ± 1.8% 797.7 ± 0.5% 1.030
railsbench 1230.6 ± 0.2% 1232.7 ± 0.2% 0.998
erubi-rails 489.6 ± 2.1% 505.0 ± 1.9% 0.969
lobsters 458.7 ± 0.8% 465.9 ± 2.7% 0.985
shipit 1117.6 ± 2.0% 1160.8 ± 1.4% 0.963
-------------- ------------- ------------- ------------
PR 描述对这张表的总结是这样的 —— 头号基准测试 chunky-png 和 psych-load 的 ratio_in_zjit 高达 96.9–99.8%,在这两个上稳定地有 1–3% 左右的改善。ratio_in_zjit 较低的其余项目,要么表现出轻微回归,要么结果不稳定。
翻译过来就是:只有在 ZJIT 编译了几乎全部代码的基准测试上才拿到了收益,其余的反而稍微变慢了。 shipit 慢了 3.7%,erubi-rails 慢了 3.1%。railsbench 基本原地踏步。要是把这总结成「ZJIT 快了 2~3%」,那就是在撒谎。
在只留下 JIT-to-JIT 调用的微基准测试上,结果是这样的。
----- ----------- ----------- ------------
bench before (ms) after (ms) before/after
fib 26.6 ± 0.1% 25.4 ± 0.1% 1.049
----- ----------- ----------- ------------
4.9% 的改善。不过这是作者为了单独观察这项改动而精心挑选出的最佳条件 —— fib 是一个除了压栈帧之外几乎什么都不干的函数。不该指望这个数字会出现在真实应用里。
内存 — 不是免费的
而且是有代价的。以下是 PR 以 lobsters 为基准报告的数值。
before after delta
code_region_bytes 11,452,416 12,296,192 +843,776 (+7%)
zjit_alloc_bytes 18,331,996 19,605,972 +1,273,976 (+7%)
代码体积增长了 7%(843KB),Rust 那侧的堆也增长了 7%(1.27MB)。原因很直白 —— 没写进帧里的那些值,得在退出的时候补写,侧出口(side exit)代码自然要相应地变大。
照着 PR 的算法推一遍是这样的:编译出来的侧出口有 76,817 个,每个都要用 movabs + mov 写 cfp->iseq,需要 14 字节,所以理论上应该增加 76,817 × 14 = 1,075,438 字节。可实际增加的只有 843,776 字节。PR 在这里做了一个有意思的推断 —— 既然代码体积的增长不到 1MB,那方法调用的内联代码看起来反而变小了。比预期少增长了大约 23 万字节,而这个差额,正是从帧压栈里省下来的代码。
像这样把自己的数字拆解到这个地步的 PR 描述并不多见。而且这套算法确实是对得上的 —— 精确到千位分隔符都能验算成功。
接下来的四个月 — 栈溢出(spill)、内联器、版本数上限
PR #16262 是刻意留了尾巴的。描述里写着 —— 「今后继续减少 exit,会缓解大部分的减速;而在 Lightweight Frames 之上修好 VM 栈溢出(spill),大概率会把整体扭转为改善。不过为了避免以后的 diff 变得太大,先把核心部分单独落地」。
那些预告过的后续工作,后来确实进来了。以下是在 master 上能看到的。
- #17387 ZJIT: Avoid eager stack spills for inlined frame pushes(k0kubun,2026 年 7 月 9 日合并,+862/-563)。正是预告过的栈溢出工作。内联后的
FrameState的JITFrame会把调用方一侧的栈槽也一并编码进去,所以只要恢复最近的一个JITFrame,该函数内联的所有 CFP 的栈槽就会一起被溢出保存。 - #16966 ZJIT: Add method inliner(nirvdrum,2026 年 6 月 13 日合并,+2564/-194)。方法内联器。作者明确说它「完全能用,但与其说是生产级内联器,不如说是个起点」,落地时默认关闭。
- #17484 ZJIT: Enable the inliner by default(nirvdrum,2026 年 6 月 30 日合并)。12 天后被翻转为默认启用。
- #17562 ZJIT: Bump the
--zjit-max-versionsdefault to 4(nirvdrum,2026 年 7 月 6 日合并)。把每个 ISEQ 编译的版本数上限从 2 提到 4。 - #17653(luke-gruber,2026 年 7 月 10 日合并)。内联
yield块帧的压栈及其到 iseq 的 JIT-to-JIT 调用。是 #16966 提到的「最大局限」——块内联——的后续。
内联器真正做的事
内联器这个 PR 里有意思的不是内联本身,而是它打开了什么。作者的原话。
Simply eliminating method dispatch by inlining method bodies is interesting, but undersells what inlining enables. The real benefit is the extra context we have for making optimization decisions.
于是 ZJIT 的 optimize 流水线被包进了一个循环里 —— 类型特化 → 内联 → 后续优化,反复执行直到内联达到不动点(fixed-point)。为了防止失控,设了一个上限,默认是 10 次(--zjit-inline-max-iterations)。
在这里,作者同样给自己的观察加上了限定语。
While working with ruby-bench I found we rarely looped more than 2 or 3 times, at least with the currently defined defaults for inline threshold and budget. As those defaults were chosen arbitrarily, that's not a very strong claim.
「这些默认值是随手选的,所以这不是一个很有力的论断。」这份坦诚原原本本地留在了源码里。以下是 zjit/src/options.rs 中,那个常量上方的注释。
/// Default --zjit-inline-threshold
/// TODO (nirvdrum 2026-06-25): 30 has proven to work well with ruby-bench, but we should finely
/// tune across more workloads.
pub const DEFAULT_INLINE_THRESHOLD: InlineThreshold = 30;
--zjit-inline-budget(默认 200)上也挂着同样形式的 TODO。也就是说,今天 ZJIT 内联器的默认值,只在 ruby-bench 上得到过验证,而作者们把这一点原原本本地记录了下来。
还有一个需要留意的数字
以下是把 --zjit-max-versions 从 2 提到 4 的 PR #17562 拿出来当依据的数字。nirvdrum 本人测量,条件是 optcarrot 单一基准测试,x86_64-linux。
--zjit-max-versions=2: #1: 2315ms #2: 2299ms #3: 2288ms
--zjit-max-versions=4: #1: 829ms #2: 734ms #3: 732ms
从 2288ms 到 732ms,是 3.1 倍。这是个大数字,所以更得看清楚条件。测量命令是 WARMUP_ITRS=0 MIN_BENCH_ITRS=3 MIN_BENCH_TIME=0 —— 没有预热,跑 3 轮。 而且 PR 给出的数据只有 optcarrot 这一项,没有 ruby-bench 的完整表。PR 正文的说法也就到这个程度 —— 「在一起研究 ZJIT 的过程中,我们发现调高 --zjit-max-versions 的值会带来相当可观的改善」。
这个数字没有告诉你的:在其他基准测试上是不是也这样、预热之后进入稳态是不是也这样、多编译一倍版本数的内存代价是多少。3.1 倍是事实,但「ZJIT 快了 3 倍」这句话,这份数据支撑不起来。
诚实的现状 — ZJIT 仍不是默认选项
这是本文的结论所在。RubyKaigi 2026 的摘要说要讲「从 YJIT 换到 ZJIT 的理由」,四个月里上面那些 PR 也都合并了。那么,ZJIT 是不是已经成了 4.1 的默认 JIT?
没有。截至今天(2026 年 7 月 16 日),ruby/ruby master 里的 ruby.c 是这样的。
#if !USE_YJIT && USE_ZJIT
DEFINE_FEATURE(jit) = feature_zjit,
#else
DEFINE_FEATURE(jit) = feature_yjit,
#endif
#if USE_YJIT
# define DEFAULT_JIT_OPTION "--yjit"
#elif USE_ZJIT
# define DEFAULT_JIT_OPTION "--zjit"
#endif
读出来就是 —— 只要构建里带着 YJIT,--jit 指的就是 YJIT。 ZJIT 只有在 YJIT 压根没被编译进去的情况下才会成为默认。而根据 #15368,只要构建环境允许,ZJIT 会和 YJIT 一起默认编译进去,所以在普通的构建里,这个条件并不成立。
还有一点值得指出 —— Ruby 从一开始就不会自动打开任何 JIT。 不管是 --yjit 还是 --zjit,都得显式指定。--jit 的意思是「打开这个构建的默认 JIT」,ruby --help 里的说明原话也正是「Enable the default JIT for the build; same as --yjit」。所以「ZJIT 不是默认选项」这句话的准确含义是这样的 —— 当你要求给一个 JIT 时,拿到的依然是 YJIT。
4.1 还没发布,计划终归是计划。真要发生这场转换,需要有一次把上面那个 #if 翻转过来的提交,而截至今天,那次提交并不存在。
什么时候不该用
归纳起来是这样。
现在不要把 ZJIT 用到生产环境。 这不是我的意见,而是打造它的团队自己说的话 —— 发布说明写着「maybe hold off on deploying it in production for now」,发布文章更进一步,写道「这是一个相当新的编译器,应该预期会出现崩溃和极端的性能下降(或提升)」。
如果你升级到 4.0 是为了性能,答案依然是 YJIT。 这里有一点要说清楚 —— 上面搬出来的那些表,不是 ZJIT 对 YJIT 的比较。它们比较的是加入 Lightweight Frames 之前和之后的 ZJIT,所以那些数字支撑不了「ZJIT 比 YJIT 慢百分之多少」这类说法。「ZJIT 还没有 YJIT 快」这个判断的依据,不是表格,而是发布说明和发布文章本身的原话 —— 两处都明确写着「not yet as fast as YJIT」。至于 4.0 这个时间点上 ZJIT 与 YJIT 之间差距的具体数字,我没能找到把它钉死下来的公开资料。发布文章指向的 rubybench 仪表盘是一张随时间变化的实时图表,所以这里没有把它上面的数字搬过来。
那到底什么时候用呢? 用 --zjit 把你的测试套件跑一遍 —— 发布文章明确建议的正是这个。要是发现崩溃或者行为异常,希望你能报告给 Ruby 的 issue 追踪器 或 GitHub。而这件事确实是值回票价的 —— 截至 4.0,ZJIT 通过了 Ruby 自身的全部测试套件、Shopify 大型应用的测试套件和影子流量、以及 GitHub.com 的测试套件。
如果你想关掉内联器。 #17484 在把内联器设为默认启用时,明确写出了代价 —— 内联会增加编译时间和内存占用。要是这成了问题,可以把阈值设为 0 来关闭它。不过 PR 正文里写的是 --zjit-inliner-threshold=0,而实际源码(zjit/src/options.rs)解析的选项名却是 inline-threshold。也就是说,真正起作用的是 --zjit-inline-threshold=0。这看起来像是 PR 描述那一侧的笔误。
这个故事与并行无关
有一点容易混淆,先在这里理清楚。ZJIT 讲的是单线程执行速度的故事。Ruby 的并行故事是另外一码事,那是 Ractor。
Ruby 4.0 的发布说明在 Ractor 这一侧也带来了相当大的变化 —— Ractor::Port 作为新的同步机制加入,Ractor.yield 和 Ractor#take 被移除;新增了对应 Thread#join/Thread#value 的 Ractor#join 和 Ractor#value;内部数据结构也做了大量改进,「以大幅减少对全局锁的竞争」。而且发布说明写道,Ractor 是在 3.0 中作为实验性功能加入的,目标是「明年」摘掉那个实验性标签。
如果你对摘掉全局锁这件事感兴趣,可以看看正面处理同一个问题的Python 3.15 的 abi3t那一篇 —— 讲的是摘掉 GIL 之后遗留下来的部署问题。Ruby 和 Python 的路线不同(Ruby 不摘 GVL,而是选择用 Ractor 来提供隔离的并行执行单元),但两门语言在差不多同一时期撞上同一类墙,这本身是个有意思的对称。Ruby 生态整体的地形,我在现代 Ruby/Rails 2026一篇里另行梳理过。
不管怎样,ZJIT 再怎么变快,都不会和 Ractor 的故事交汇。这是两条不同的轴。
结语
归纳起来是这样。Ruby 4.0 上线了 ZJIT,却没有打开它,发布说明自己写着「还没有 YJIT 快」。瞄准 4.1 的工作核心是 Lightweight Frames,想法很简单 —— 既然每次方法调用都老老实实填进帧里的那些元数据,大部分根本没人读,那就只写一个指针,其余的等到真正被读取时(比如回溯、异常、Binding)再恢复。
这个想法之所以有价值,不是因为它新,而是因为它是第二次尝试。2023 年的 frame outlining 在每次 cfp 读取时都加了一个分支,结果拖慢了解释器,被放弃了。Lightweight Frames 是在清楚这个死因的前提下设计出来的,所以 PR 的第一张基准测试表,证明的正是「解释器没有变慢」这件事。
而那个 PR,在大多数基准测试上什么都没能变快。作者本人就是这么写的。代码体积涨了 7%,堆也涨了 7%。收益只出现在 ZJIT 编译了 97% 以上代码的那两个基准测试上,幅度是 1–3%。那就是三月份诚实的样子,而在那之上,栈溢出、内联器和块内联又花了四个月的时间叠加上去。
所以今天的答案是这样的 —— 生产环境请用 YJIT。ZJIT 现在还处在「拿测试套件跑一遍、坏了就报告」这个阶段。而「Ruby 4.1 中 ZJIT 会成为默认选项」这句话,目前还只是计划,不是事实。等到 ruby.c 里的那个 #if 翻转过来,它才会变成事实。那次提交才是这个故事真正的结局,大概五个月后就能见分晓。
如果打地基看起来很枯燥,那本来就是打地基该有的样子。
参考资料
- Ruby 4.0.0 Released — 官方发布说明 (2025-12-25)
- ZJIT is now available in Ruby 4.0 — Max Bernstein, Rails at Scale (2025-12-24)
- Shopify/ruby#909 — ZJIT: Lightweight Frames 设计文档(含 prior art)
- ruby/ruby#16262 — ZJIT: Lightweight Frames (合并于 2026-03-27,基准测试与内存数值)
- ruby/ruby#16966 — ZJIT: Add method inliner (合并于 2026-06-13)
- ruby/ruby#17484 — ZJIT: Enable the inliner by default (合并于 2026-06-30)
- ruby/ruby#17562 — ZJIT: Bump the --zjit-max-versions default to 4 (合并于 2026-07-06)
- ruby/ruby#17387 — ZJIT: Avoid eager stack spills for inlined frame pushes (合并于 2026-07-09)
- ruby/ruby#17653 — ZJIT: inline yield block frame pushes (合并于 2026-07-10)
- ruby/ruby#10080 — YJIT: Lazily push a frame for specialized C funcs (合并于 2024-02-23,前人工作)
- zjit/src/options.rs — 默认值与 TODO 注释的出处
- Lightning-Fast Method Calls with Ruby 4.1 ZJIT — k0kubun, RubyKaigi 2026 (函馆,4 月 22–24 日)
- A new Register Allocator for ZJIT — Aaron Patterson, Rails at Scale (2026-05-27)
- Ruby — endoflife.date (发布日期交叉核对)