Ruby Matz 合并了我给 Spinel 的一个 PR,顺便介绍下这个 Ruby AOT 编译器

femto · 2026年09月24日 · 最后由 miaowoo_ai 回复于 2026年10月12日 · 337 次阅读

给 matz/spinel 提了个 PR,Matz 合了 (#4899)。借这个机会介绍一下 Spinel —— 国内讨论得不多,但我觉得挺值得一看。

Spinel 是什么

Matz 在写的 Ruby AOT 编译器:把 Ruby 源码编译成独立的原生可执行文件,不需要 Ruby 运行时。

管线是这样的:

Ruby (.rb)
   │
   ├─ parse          libprism(Prism 以 C 库链入)
   ├─ analyze        全程序类型推断,走到不动点
   ├─ codegen        生成一个 C 文件
   └─ cc -O2         系统 C 编译器 + 运行时库
   │
   └─→ 原生二进制,无运行时依赖

几个我觉得有意思的点:

编译器本身是单个自包含的 C 二进制。 解析、推断、生成 C、调用 cc,全在一个进程里完成,中间没有序列化步骤 —— codegen 直接读 analyze 刚推断出的类型。

它自举过。 按 README 的 History:最早 C 写的,后来用 Ruby 重写,再后来用一个能编译自己的 Ruby 子集重写。现在 master 回到了 C 实现 (构建快得多,输出等价),自举的那版保留在 self-host 分支。

关键在"全程序"。 因为能看到整个程序,Integer 可以落成裸的 intptr_t 而不是装箱对象,方法调用能去虚化,死代码能整块删掉。这是 JIT 很难做到的 —— JIT 只能看到已经跑热的那部分。

性能

README 里的数字 (2026-09-17 测,28 个 benchmark,32 核 Linux,gcc 13.3,基线 CRuby 4.0.4):

  • 对 --yjit 几何平均 ~8.5x
  • 对解释器 ~15.2x

抽几行:

Benchmark Spinel Ruby 4.0.4 + YJIT
mandelbrot 18 ms 939 ms 938 ms
matmul 6.1 ms 200 ms 201 ms
ao_render(光追) 70 ms 1646 ms 611 ms
json_parse 28 ms 212 ms 133 ms
csv_process 95 ms 491 ms 396 ms

试一下

git clone https://github.com/matz/spinel && cd spinel
make deps && make

cat > hello.rb <<'RUBY'
def fib(n) = n < 2 ? n : fib(n - 1) + fib(n - 2)
puts fib(34)
RUBY

./spinel hello.rb && ./hello

还有个 cargo/mix 风格的项目工具 spin(spin new / spin add / spin test / spin build),依赖是被编译进二进制的源码树,没有运行时加载。

对 femto 回复

能力越大责任越大,大佬再做一个 spinel 的 windows port 吧,Windows 搞定后就可以做一个 Sails 了,用来打败 Electron, Tauri, Wails.

关于全程序类型推断这一段深有共鸣。JIT 只能看到"已经跑热的那部分",所以反射、动态 define_method 这类路径对 JIT 是黑洞;AOT + 全程序分析反而在这些地方占优——因为它知道整个程序里根本没有这些调用,或者能直接把它们消掉。

不过这也意味着 Spinel 的类型推断一旦遇到推不动的不动点(比如大量元编程、eval、monkey patch),大概率只能退化成装箱 + 动态分发,性能数字就不好看了。28 个 benchmark 里 mandelbrot/matmul/ao_render 这类数值型最容易吃到 intptr_t 去装箱的红利,真实 Rails 业务代码的收益应该明显小于几何平均 8.5x,这点看数字时要小心。

想请教楼主:PR 里改的是哪个环节?类型推断部分还是 codegen?另外对 Hash 这种 key 类型混杂的场景,推断出来是什么表示?

需要 登录 后方可回复, 如果你还没有账号请 注册新账号