1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Spinelすげえ! Rubyの動的型付けがそのままCに!

1
Posted at

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
)

となっています。

an は、

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))

となっています。

bsp_int として扱われていますが、比較するときには sp_box_int()sp_RbVal に変換され、sp_RbVal型のlv_xに揃えられている様に見えます。

さらに sp_poly_lt() の中では、sp_RbValtag などを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すげえ!

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?