RubyのAOTコンパイラって何だ?
まつもと ゆきひろさんがRubyのAOT(Ahead Of Time : 事前)コンパイラ「Spinel」をリリースされましたね。
https://github.com/matz/spinel
これの開発エピソードが日系ソフトウェア2026年9月号の「プログラミング質問箱」に書かれていてとても面白くてためになりました! 神回でした! Matz、やってくれるぜ!
Rubyってインタープリタ言語ですよね?
事前「コンパイラ」とは?
なんと、これによって
Ruby→C言語→バイナリファイル ができるそうな!
これができると
- Ruby成果物配布の課題(ここでは動くけどあっちでは動かない)
が解決し、更に
- Ruby成果物の速度アップ(こちとらバイナリですから)
も実現できると。
すげえじゃねえか何だそれ!
Spinelを使ったらRubyが速くなるか確かめてみた
手前味噌ではありますが題材として以下の記事とまったく同じ問題「最小自由数」を同じ環境で解きました。
https://qiita.com/tj9999/items/4131016df5f1c5730584
99999件のデータを入力し、正しい答え69918にたどり着く処理を、
「素のRubyのみ」と「Ruby + Spinel」で10万回ずつ行い、平均処理速度を見比べてみました。
- 確認環境
| 項目 / Item | 内容 / Contents |
|---|---|
| OS | Fedora 41 (x86_64) |
| CPU | Intel(R) Core(TM) i3-4130 CPU @ 3.40GHz |
| Memory | 8GB |
素のRubyのみ
- 確認環境
~/Ruby/spinel/myapp/bin$ ruby --version
ruby 3.3.10 (2025-10-23 revision 343ea05002) [x86_64-linux]
- 測定対象プログラム
# myapp_cruby.rb
require_relative 'input_min_free'
def minfrom(a, n, xs)
return a if n == 0
b = a + 1 + n / 2
us, vs = xs.partition { |x| x < b }
m = us.length
if m == b - a
minfrom(b, n - m, vs)
else
minfrom(a, m, us)
end
end
def minfree(xs)
minfrom(0, xs.length, xs)
end
sum = 0.0
best = Float::INFINITY
worst = 0.0
best_count = 0
worst_count = 0
ans = 0
100000.times do |i|
# 入力データ99999件は別ファイル input_min_free.rb に定義
v2 = InputMinFree::INPUT_MIN_FREE.dup
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
ans = minfree(v2)
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
cur = end_time - start_time
best, best_count = cur, i if cur < best
worst, worst_count = cur, i if cur > worst
sum += cur
end
puts "result:#{ans}"
puts "sum = %.6f sec." % sum
puts "ave = %.6f sec." % (sum / 100000)
puts "best = %.6f sec." % best
puts "best_count = #{best_count}"
puts "worst = %.6f sec." % worst
puts "worst_count = #{worst_count}"
- 測定結果
10万回測定平均 約 18ms でした。
~/Ruby$ ruby myapp_cruby.rb
result:69918
sum = 0.181034 sec.
ave = 0.018103 sec.
best = 0.017671 sec.
best_count = 5
worst = 0.018359 sec.
worst_count = 9
Ruby + Spinel
- 確認環境
~/Ruby/spinel/myapp/bin$ ruby --version
ruby 3.3.10 (2025-10-23 revision 343ea05002) [x86_64-linux]
/Ruby/spinel/myapp/bin$ spin --version
spin 2026.09.12+29 (fa630b1f) [gcc 14.3.1 (cc)]
- 測定対象プログラム
# frozen_string_literal: true
require_relative 'input_min_free'
# 処理時間測定にはCライブラリを使用
module LibC
ffi_func :clock, [], :long
ffi_const :CLOCKS_PER_SEC, 1_000_000
end
def minfrom(a, n, xs)
return a if n == 0
b = a + 1 + n / 2
us, vs = xs.partition { |x| x < b }
m = us.length
if m == b - a
minfrom(b, n - m, vs)
else
minfrom(a, m, us)
end
end
def minfree(xs)
minfrom(0, xs.length, xs)
end
sum = 0.0
best = Float::INFINITY
worst = 0.0
best_count = 0
worst_count = 0
ans = 0
1000_00.times do |i|
# 入力データ99999件は別ファイル input_min_free.rb に定義
v2 = InputMinFree::INPUT_MIN_FREE.dup
start_time = LibC.clock()
ans = minfree(v2)
end_time = LibC.clock()
# clock() の差分を秒に換算
cur = (end_time - start_time).to_f / LibC::CLOCKS_PER_SEC
if best > cur
best = cur
best_count = i
end
if worst < cur
worst = cur
worst_count = i
end
sum += cur
end
puts "result:#{ans}"
puts "sum = %.6f sec." % sum
puts "ave = %.6f sec." % (sum / 100_000)
puts "best = %.6f sec." % best
puts "best_count = #{best_count}"
puts "worst = %.6f sec." % worst
puts "worst_count = #{worst_count}"
- 測定結果
10万回測定平均 約 3.6ms でした。
~/Ruby/spinel/myapp/bin$ spinel myapp.rb -o myapp
./myapp
myapp.rb -> myapp
result:69918
sum = 368.427699 sec.
ave = 0.003684 sec.
best = 0.003282 sec.
best_count = 22
worst = 0.007330 sec.
worst_count = 14944
Ruby と Spinel の 速度比較結果
筆者の題材(最小自由数)と環境では
SpinelでコンパイルしたRubyプログラムは、素のRubyとして実行した場合と比べて約5倍高速でした!
| 環境 / Environment | バージョン / Version | 10万回の平均処理速度[ms] / Avg Time |
|---|---|---|
| Ruby | ruby 3.3.10 (2025-10-23 revision 343ea05002) [x86_64-linux] | 18.103 |
| Ruby + Spinel | spin 2026.09.12+29 (fa630b1f) [gcc 14.3.1 (cc)] | 3.684 |
Spinelが何をしているのか調べてみた
RubyをCにするって結局何だ?
ていうか無理だろRubyは動的型付けインタープリタ言語、Cは静的型付けコンパイラ言語だぞ?
じゃあさっき速くなったあれは何だ? いったい俺は何をされたんだ!?
…速くなったってどれくらいだっけ?
前回の記事で最速だった C++ と比較してみよう
| 環境 / Environment | バージョン / Version | 10万回の平均処理速度[ms] / Avg Time |
|---|---|---|
| Ruby + Spinel | spin 2026.09.12+29 (fa630b1f) [gcc 14.3.1 (cc)] | 3.684 |
| C++ | gcc (GCC) 14.3.1 20250808 (Red Hat 14.3.1-3) | 0.718 |
今回の題材の結果で見ると
SpinelはRubyよりは速いがC++ほどではなかった。
上記C++はO3でビルドした結果で、
Spinelはcc(筆者環境ではgcc 14.3.1)をO2で実行するので、
念のためC++の方もO2でビルドしてみましたが速度はほぼ同じでした。
ということで改めて、今回の題材でSpinelは何をしていたのだろう?
Spinelで一気に実行ファイルを作るのではなく、オプションをつけてC言語作成の段階で止めてもらってみます。
spinel myapp.rb -c
すると、
Wrote myapp.c
と表示されました。
では、最小自由数の核心部分を見てみます。
Rubyでは、
us, vs = xs.partition { |x| x < b }
と書いていたところです。
生成されたCには、こんなコードがありました。
sp_PolyArray *_t2 = sp_poly_enum_recv(lv_xs, "partition");
SP_GC_ROOT(_t2);
sp_PolyArray * _t3 = _t2;
SP_GC_ROOT(_t3);
sp_PolyArray *_t4 = sp_PolyArray_new();
SP_GC_ROOT(_t4);
sp_PolyArray *_t5 = sp_PolyArray_new();
SP_GC_ROOT(_t5);
for (sp_int _t6 = 0;
_t6 < sp_PolyArray_length(_t3);
_t6++) {
lv_x = sp_PolyArray_get(_t3, _t6);
if (sp_poly_lt(lv_x, sp_box_int(lv_b)))
sp_PolyArray_push(_t4, sp_PolyArray_get(_t3, _t6));
else
sp_PolyArray_push(_t5, sp_PolyArray_get(_t3, _t6));
}
Rubyの
xs.partition { |x| x < b }
が、Cでは
for (...) {
...
}
になっています。
次に、上記「xs.partition」のxsの部分を見ていきます。
xsは、最小自由数を見つける問題のための入力データ(正の整数の配列)です。
myapp.c の minfrom() の引数を見ると、
static inline sp_int sp_minfrom(
sp_int lv_a,
sp_int lv_n,
sp_RbVal lv_xs
)
となっています。
a と n は、
sp_int lv_a
sp_int lv_n
なのに、
xs → sp_RbVal
です。
なんだ sp_RbVal って?
Spinelの中身の方を検索していくと以下の定義がありました。
typedef struct {
int tag;
int cls_id;
union {
sp_int i;
const char *s;
sp_float f;
sp_bool b;
void *p;
} v;
} sp_RbVal;
sp_RbVal は、Rubyの動的な型の値を表現するためのCの構造体に見えます。
今回の題材では、Rubyの変数「xs」は、minfrom() の引数として受け取られており、生成されたCコードでは sp_RbVal 型として扱われています。
一方、実験として同じ [43308,43319,...] という整数配列を定数として定義しただけのRubyからSpinelでCを生成したときは、型が sp_IntArray となっていました。
なぜ同じ整数配列なのに表現が違うのかはわかっておりませんが、今回は関数の引数として渡された xs をコンパイル時に sp_IntArray と言い切ることができなかったため、sp_RbVal 型として扱われたのだと認識しています。
そして今注目している処理「xs.partition { |x| x < b }」の x < b は、
sp_poly_lt(lv_x, sp_box_int(lv_b))
となっています。
b は sp_int として扱われていますが、比較するときには sp_box_int() で sp_RbVal に変換され、sp_RbVal型のlv_xに揃えられている様に見えます。
さらに sp_poly_lt() の中では、sp_RbVal の tag などをlv_xとlv_bの比較に使っているのかも知れないです。
つまり、ここまで追いかけて見えてきたことは
Ruby
↓
Spinel
↓
Rubyの動的な(実行時まで型を決めない)値を
Cのstruct + union + tagで表現
↓
Cコード生成
↓
GCC
↓
ネイティブバイナリ
という流れです。
SpinelはRubyの動的型付けを捨ててCの静的型付けに無理やり押し込んでいるのではなく、
Rubyの動的な値を扱う仕組みそのものをCで実装している。
これはすごいと思いました。
いやまだ全然わかっちゃいないんだけど。
そして、ここでC++版のコードを思い出します。
C++版では、
int x;
int b;
if (x < b) {
...
}
です。
一方Spinelでは、
sp_RbVal x;
sp_int b;
sp_poly_lt(x, sp_box_int(b));
となっています。
この違いを見ると、
「SpinelはRubyをCにしているが、C++の int のように値の型を単純な整数だけに絞り込んでいるわけではない。必要に応じて sp_RbVal で動的な型として値を表現している」
ということが読み取れます。
今回、
Ruby 18.103 ms
Spinel 3.684 ms
C++ 0.718 ms
となった理由を考えると、このあたりも関係していそうです。
ただし、ここで「intじゃなくてsp_RbVal だから遅い」と断定するのはまだ早いです。
sp_PolyArray_get()
sp_PolyArray_push()
sp_poly_lt()
などが実際に何をしているのか。
ここまで掘ると、もう少し具体的に「SpinelがどんなCを生成して、どこでコストが発生しているのか」が見えてきそうです。
……が、今回は一旦ここまでとします。
現時点の感想
Spinelすげえ!
Matzすげえ!