Spinelは、Rubyの作者まつもとゆきひろ(Matz)氏がGitHubのmatz/spinelで開発しているRubyのAOT(事前)コンパイラです。RubyのソースをC言語に変換し、システムのCコンパイラで単一のネイティブ実行ファイルにします。2026年4月のRubyKaigi 2026で発表され、9月12日に最初のリリース2026.09.12が出ました。この記事では、2026年10月8日時点のmasterを手元のmacOSでビルドし、導入手順、ベンチマークの読み方、コンパイルを拒否されるRubyの書き方を実測で確かめた結果をまとめます。
まとめ:Spinelの要点(2026年10月時点)
- RubyをPrismで構文解析し、プログラム全体の型推論を経てCを出力、
ccでネイティブバイナリにする。実行時にRubyは不要 - 2026年6月にコンパイラ本体をRuby製の自己ホスト版からC実装へ置き換えた。5月の報道(miniruby比11.6倍、Thread非対応)は旧版の情報
- 現行READMEの公表値はRuby 4.0.4 +YJIT比で幾何平均約8.5倍(28ベンチマーク)。fibの44.6倍はCコンパイラの定数畳み込みが効いた例外値
- 導入はソースから
make deps && make。gem install spinelは無い。プロジェクト管理は付属のspinが担う eval、method_missingのディスパッチ、実行時に名前と本体を作るdefine_method、ObjectSpace.each_objectなどは使えない。実行時の名前によるsendは部分対応にとどまる- 文字列リテラルはfrozen扱いで、CRuby 4.0では動く
"abc" << "d"がFrozenErrorになる。既存コードの移植ではここが最初につまずく点
結論として、CPU処理が重いCLIや、Ruby無しで配りたい小さなツールなら試す価値があります。Railsアプリや既存gemに依存するコードをそのまま速くする道具ではありません。以下、根拠と手順を順に示します。
Spinelの仕組みとmatz/spinelの開発経緯
RubyをC経由で単一バイナリにする処理の流れ
READMEが示すパイプラインは、構文解析、型推論、コード生成、Cコンパイルの4段です。構文解析はRuby 3.4以降の標準パーサーPrism(C版のlibprism)をリンクして行います。次の解析段(src/analyze*.c)が引数・戻り値・インスタンス変数の型を不動点に達するまで推論し、コード生成段(src/codegen*.c)が1本のCファイルを出力します。最後にシステムのcc -O2でランタイムlibspinel_rt.aとリンクします。
動的言語のRubyをCに落とせるのは、プログラム全体を一度に読んで「この変数は常にInteger」と確定させるからです。2つの呼び出し元で型が食い違うと、その引数はタグ付き共用体(README上のpoly)に広げられ、速度が落ちます。--warn-widenを付けると、型が広がった引数や戻り値ごとに警告が出ます。
2026年3月の初コミットから2026.09.12リリースまで
| 日付 | 出来事 | 出典 |
|---|---|---|
| 2026-03-16 | 初コミット | git log |
| 2026-04-22〜24 | RubyKaigi 2026(函館)で発表 | rubykaigi.org |
| 2026-05-04 | FFIの文書を追加 | docs/FFI.md |
| 2026-06-16 | C実装への置き換えをREADMEに反映 | git log |
| 2026-07-01〜03 | Thread対応とspinの文書を追加 | docs/thread.md・spin.md |
| 2026-09-12 | 最初のリリース2026.09.12 | GitHub Releases |
| 2026-09-17 | WebAssembly出力の文書を追加 | docs/wasm.md |
READMEの「History」によると、Spinelは最初C(c-versionブランチ)で書かれ、次にRuby(ruby-v1)、さらに自分自身をコンパイルできるRubyのサブセットで書き直されました。現在のmasterは解析とコード生成をCで新たに実装したもので、自己ホスト版はself-hostブランチに残っています。The Registerが2026年5月6日に「3回作り直された」と報じたのは、この経緯のうち自己ホスト版までの段階です。
開発にはAIが深く関わっています。2026年10月8日時点の総コミット14,734件のうち、10,564件(約72%)のコミットメッセージにCo-Authored-By: Claudeの行があります。READMEの貢献ルールも、AIの支援を受けたコミットにはこの行を付けるよう求めています。開発の速さも特徴で、2026.09.12のリリースから26日でmasterは7,037コミット先に進みました。
2026年5月の報道と現行版の違い
Spinelを日本語で調べると、2026年5月前後の速報記事が多く見つかります。その多くは自己ホスト版の時点の内容で、現行版とは前提が違います。docs/limitations.mdにも「Now supported (older write-ups are stale here)」という節があり、古い解説が実態に合わなくなっていることを開発側が明記しています。
| 項目 | 2026年5月の報道時点 | 2026年10月のmaster |
|---|---|---|
| コンパイラ本体 | Ruby製(spinel_codegen.rb 20,851行) | Cの単一バイナリ |
| 速度の比較相手 | miniruby(Ruby 4.1.0dev)比11.6倍 | Ruby 4.0.4 +YJIT比 約8.5倍 |
| Thread | 非対応 | GVLなしで並列実行 |
| 依存ライブラリ | RubyGems不可 | spinパッケージ、bundler-spinel |
| 出力先 | ネイティブのみ | ネイティブ+wasm32-wasi |
| リリース | なし | 2026.09.12(日付タグ) |
速度の数字は、比較相手が変わった点に注意が必要です。minirubyはCRubyのビルド途中で作られる拡張ライブラリ抜きの最小構成で、JITを使いません。現行READMEはYJIT有効のRuby 4.0.4を比較基準にしています。比較対象だけでなくSpinel本体や測定条件も異なるため、旧版の11.6倍と現行版の約8.5倍をそのまま性能の増減として比較することはできません。インタプリタ比では約15.2倍になっています。
ベンチマークの読み方:YJIT比約8.5倍の内訳
README公表値(2026-09-17計測)
コミット47225b489時点のREADMEの表は、32コアのLinuxマシンとgcc 13.3で、perf stat -r 5による5回平均を取ったものです。28本の幾何平均が約8.5倍で、個別の倍率には大きな幅があります。
| ベンチマーク | Spinel | Ruby 4.0.4 +YJIT | 倍率 |
|---|---|---|---|
| mandelbrot | 18 ms | 938 ms | 53.6倍 |
| fib(再帰) | 1.1 ms | 49 ms | 44.6倍 |
| nqueens | 6.0 ms | 141 ms | 23.6倍 |
| ao_render(レイトレーサー) | 70 ms | 611 ms | 8.7倍 |
| json_parse | 28 ms | 133 ms | 4.7倍 |
| csv_process | 95 ms | 396 ms | 4.2倍 |
| io_wordcount | 14 ms | 44 ms | 3.1倍 |
fibの44.6倍について、READMEは「誇るのではなく注記すべき行」と自ら断っています。fib(42)の呼び出し木はリテラルだけで決まる純粋関数なので、Cコンパイラがビルド時に大半を畳み込み、実行される命令は約2.3億にとどまります(再帰をそのまま実行すれば約8.7億回の呼び出し)。倍率が小さいのは、行読み込みとハッシュ更新が中心のio_wordcountと、メモリ確保が中心のcsv_processです。自分のプログラムの効果を見積もるなら、整数・浮動小数点のループが多いほど上側、文字列とI/Oが多いほど下側の数字が目安になります。
Intel Macでfib(34)を測った結果
READMEのクイックスタートにあるfib(34)を、Intel Core i9-9880H(8コア)のmacOSで測りました。Spinelは2026.09.12+7037(47225b489)、比較相手は手元のRuby 4.0.6です。値は/usr/bin/time -pの実時間で、3回とも同じでした。
| 実行方法 | 実時間 |
|---|---|
| Spinelで生成したバイナリ | 0.06秒 |
ruby --yjit hello.rb |
0.32秒 |
ruby hello.rb(インタプリタ) |
約1.0秒 |
spinel hello.rb(コンパイル自体) |
1.19秒 |
生成されたバイナリは248,384バイトで、otool -Lで見たリンク先はmacOSのlibSystemだけでした。一方、コンパイルに1.2秒かかるため、一度しか実行しない短いスクリプトではrubyで直接動かす方が早く終わります。Spinelが得をするのは、同じバイナリを何度も実行する場合と、Rubyが入っていない環境へ配る場合です。
Spinelのインストールとコンパイル手順
make depsとmakeでのビルド
Spinelはソースからビルドします。READMEは「RubyGemとしては配布しておらず、gem install spinelは無い」と明記しています。必要なのはCコンパイラ(Linuxならgccかclang、macOSならclang)と、依存ソースの取得に使うcurlです。
git clone https://github.com/matz/spinel.git
cd spinel
make deps # prism 1.9.0 と rbs 4.0.1 の gem を rubygems.org から取得し C ソースを展開
make # コンパイラ spinel とプロジェクトツール spin をビルド
./spinel --version
# spinel 2026.09.12+7037 (47225b489) [apple clang 21.0.0 (cc)]
バージョン表記の+7037は、リリース2026.09.12から7,037コミット進んだビルドであることを示します。リリース時点に固定したい場合は、GitHub Releasesのソースアーカイブspinel-2026.09.12.tar.xzを使います。依存ソースが同梱されているので、ネットワーク無しでmakeだけで組めます。sudo make installで/usr/localに入り、make install PREFIX=$HOME/.localで場所を変えられます。
spinelコマンドで単一ファイルをコンパイル
1ファイルのスクリプトなら、コンパイラspinelを直接使います。例えば、次の内容をhello.rbとして保存します。
def fib(n)
n < 2 ? n : fib(n - 1) + fib(n - 2)
end
puts fib(34)
コンパイル後に./helloを実行すると、5702887を出力します。出力ファイル名は拡張子を除いたソース名になります。
./spinel hello.rb # ./hello を生成
./spinel app.rb -o myapp # 出力名を指定
./spinel app.rb -c # app.c だけを書き出す
./spinel app.rb -S # 生成した C を標準出力へ
./spinel -E app.rb a b c # コンパイルして ARGV=[a, b, c] で実行し、バイナリは残さない
./spinel -e 'puts 42' # インラインのコードをコンパイル(出力名は a)
-cで書き出したCファイルは、lib/のヘッダーとlibspinel_rt.aがあれば別のマシンでコンパイルできます。ビルド環境と配布先を分けたいときに使えるオプションです。
spinによるプロジェクト作成と実行
複数ファイルのアプリや依存パッケージを扱うときは、付属のspinを使います。READMEはcargoやmixに近い道具だと説明しています。
$ spin new myapp
created myapp/
$ cd myapp && spin run
build myapp
Hello from myapp
$ spin add ansi --version "~> 1.0" # spin のパッケージ索引から追加
$ spin add mylib --path ../mylib # ローカルのライブラリを追加
# test/smoke.rb などにテストコードを保存してから実行
$ spin test # 保存済み期待出力、なければ CRuby の出力と比較
spin newはspin.toml(依存関係を書くマニフェスト)、bin/myapp.rb、test/を作ります。依存パッケージは実行時に読み込まれるのではなく、ソースごと1本のバイナリへコンパイルされます。spin testは保存済みの期待出力があればそれと比較し、なければ同じテストをCRubyでも実行して出力を比較します。期待出力をCRubyから更新する場合はspin test --regenを使います。
Spinelで書けないRuby:コンパイル時に拒否される機能
docs/limitations.mdは、制約を「AOTの仕組み上変わらないもの」「今後広げられるもの」「意図的にCRubyと変えたもの」に分けて一覧にしています。仕組み上使えない主なものは次のとおりです。
| 機能 | 扱い |
|---|---|
| eval、文字列を渡すinstance_eval | 非対応(ブロック形式は可) |
| method_missing | 定義しても呼ばれない |
| 動的send・define_method | sendは部分対応、define_methodはリテラル名のみ |
| ObjectSpace・TracePoint・binding | 下記の例外を除き非対応 |
| ObjectSpaceのfinalizer登録・解除 | 対応 |
| binding.local_variable_get(:x) | リテラル名なら対応 |
| Refinements(refine、using) | 効かない |
| Class.new(parent) { } | 原則非対応(Array継承のブロック形式は対応) |
| Hash、Stringなど組み込みクラスの継承 | 拒否(Arrayの継承は可) |
| Time.parse、Time.strptime | 拒否(Time.nowやTime.atは可) |
| 文字コード | UTF-8・ASCII-8BITのみ対応 |
使えない機能に当たると、コンパイラはspinel: ファイル名:行番号:の形式でその構文を名指しし、何も書き出さずに失敗します。この拒否はメソッド単位で処理され、1回のコンパイルで複数のメソッドの拒否をまとめて報告します。CRubyから持ってきたコードの直すべき箇所を一度に洗い出せるので、移植ではまずコンパイルにかけて一覧を取るのが近道です。
evalと実行時のsendの拒否メッセージ
次のコードをコンパイルすると、method_missingには警告、evalには拒否が出ました。
class Foo
def method_missing(name, *args)
"missing #{name}"
end
end
code = "1 + 2"
puts eval(code)
spinel: ng.rb:2: warning: method_missing is defined but spinel does not dispatch undefined-method calls to it; such calls raise NoMethodError (method_missing can still be called explicitly)
spinel: ng.rb:7: unsupported eval of a runtime string is not supported by AOT compilation (define the code statically): ...
spinel: 1 refusal, nothing written
def g(o, m) = o.send(m)をIntegerレシーバーと実行時の名前で呼び出した検証では、「AOT needs a compile-time-known name」という理由で拒否されました。send(:name)のようにリテラルで書けば通ります。docs/limitations.mdによると、明示的なレシーバーへの実行時の名前によるsendは部分対応で、プログラム中にシンボルか文字列のリテラルとして現れるメソッド名への分岐に変換されます。それ以外の名前は実行時にNoMethodErrorになります。コンパイルを拒否された場合は、case文で分岐する形に書き換えます。
文字列リテラルはfrozen:CRuby 4.0と結果が変わる例
移植で最初に当たりやすいのが文字列の扱いです。Spinelでは文字列リテラルが既定でfrozenなので、次のコードは実行時にFrozenErrorになります。
s = "abc"
s << "d"
puts s
$ spinel fr.rb && ./fr
fr.rb:2: warning: `s` only ever holds frozen string literals, so this `<<` raises FrozenError at run time ...
can't modify frozen String: "abc" (FrozenError)
$ ruby fr.rb # Ruby 4.0.6
abcd
CRuby 4.0のリテラルは書き換えると非推奨警告を出す「chilled string」で、エラーにはなりません。Spinelで追記用の文字列を作るときは+"abc"かString.new("abc")と書きます。コンパイラが警告を出してくれるので、警告を読み飛ばさないことが対策になります。
整数オーバーフローは既定で例外
Spinelの整数は固定幅のネイティブ整数です。既定の--int-overflow=raiseでは、対象環境の固定幅整数の範囲を超えると例外になります。幅はamd64・arm64では64ビット、wasm32などの32ビットターゲットでは32ビットです。
$ cat ov.rb
x = 9223372036854775807
puts x + ARGV.size + 1
$ spinel ov.rb && ./ov
integer overflow in + (RangeError)
$ spinel ov.rb --int-overflow=promote -o ov2 && ./ov2
9223372036854775808
CRubyと同じく多倍長整数へ自動で切り替えたいなら--int-overflow=promoteを、固定幅整数の折り返しを意図するハッシュ計算やチェックサムなどではwrapを指定します。このモードは検査を省く代わりに、範囲を超えた計算結果が通知なく折り返します。q = q * kのような明らかに桁が増える累積は、既定のモードでも多倍長へ昇格されます。
FFI・Thread・WebAssemblyの対応範囲
ffi_funcによるC関数の直接呼び出し
拡張ライブラリをビルドせずに、モジュール内の宣言だけでCの関数を呼べます。docs/FFI.mdの例をそのままコンパイルすると、strlenの結果12が返りました。
module LibC
ffi_func :strlen, [:str], :size_t
ffi_func :getpid, [], :int
end
puts LibC.strlen("hello, world") # 12
スカラー、文字列、不透明ポインタ、整数定数、バイトバッファ、構造体フィールドの読み取りに対応しています。リポジトリのexamples/ffi/にはsqlite3を呼ぶ例もあります。
GVLなしのThreadを4本で動かした結果
現行版のThreadはGVL(グローバルVMロック)を持たず、M:Nスケジューラがグリーンスレッドを1コア1本のOSワーカーに割り当てます。整数ループを4本のThreadで回し、ワーカー数を変えて実時間を比べました。
def work(n)
s = 0
i = 0
while i < n
s += i % 7
i += 1
end
s
end
threads = 4.times.map { Thread.new { work(200_000_000) } }
puts threads.map(&:value).sum
$ spinel th.rb
$ SPINEL_WORKERS=1 /usr/bin/time -p ./th # real 1.57
$ /usr/bin/time -p ./th # real 0.40(8コアのIntel Mac)
ワーカーを1本に絞ると1.57秒、既定では0.40秒で、4本のThreadがほぼ並列に動いています。
その代わり、CRubyのGVLが暗黙に守っていた共有データの排他は自分で行う必要があります。READMEは、同期なしの共有データの書き換えはJRubyやTruffleRubyと同じくデータ競合になると明記しています。Threadを使うプログラムだけに別のランタイムがリンクされるので、Threadを使わないプログラムの速度は変わりません。
wasm32-wasiでのWebAssembly出力
spinel --target=wasm32-wasi app.rbでapp.wasmを出力できます。wasi-sdk 34以降と、make wasm-rtでビルドするWasm用ランタイムが必要で、既定のビルドには含まれません。実行には例外処理提案(exnref形式)に対応したエンジンが要り、docs/wasm.mdはwasmtime 48と現行のChrome、Firefox、Safari、Nodeを挙げています。
CRuby・mruby・Spinelの使い分けと採用を見送る場面
| 観点 | CRuby(YJIT・ZJIT) | mruby | Spinel |
|---|---|---|---|
| 実行形態 | インタプリタ+JIT | 組み込み用VM | ネイティブバイナリ |
| 実行時のRuby | 必要 | VMを組み込む | 不要 |
| eval・メタプログラミング | すべて可 | mrbgemで一部可 | ほぼ不可 |
| 既存gem | そのまま | mrbgem | spinパッケージ |
| Windows | 対応 | 対応 | WSL推奨 |
同じ「Rubyを速くする」でも、YJITやZJITはCRubyの中で動くJITなので、Rubyのコードはそのまま動きます。JITとAOTの違いはJITコンパイラの仕組みとAOTとの違いで詳しく扱っています。Spinelは書けるRubyを絞り、CRuby本体を不要にする道具です。生成バイナリには、プログラムに必要なSpinelのランタイムがリンクされます。向いているのは、計算の重いCLI、Rubyの入っていないCIやコンテナへ1ファイルで配るツール、C関数を薄く包むユーティリティです。
次のどれかに当てはまるなら、現時点では採用を見送るべきです。第一に、業務システムの本番処理です。リリースは2026.09.12の1本だけで、READMEはこのリリース名について「コンパイラの完成度を何も主張しない」と書いています。第二に、DSLやmethod_missingに依存するライブラリを中心に組んだコードです。第三に、Windowsのネイティブ環境が必須の配布物です。ネイティブのWindows版はコミュニティによるベストエフォートです。CIにはLinux上でのクロスコンパイル確認がありますが、実行テストではなく、失敗してもpushを阻止しません。
RailsアプリとRubyGemsの対応範囲
Railsアプリをそのままspinelにかけることはできません。READMEが周辺プロジェクトとして挙げるroundhouseは、Railsのモデル・コントローラー・ビュー・ルーティングをメタプログラミング無しの素のRubyに変換し、Spinelでコンパイルできる形にします。READMEによれば、この方法で37signalsのチャットアプリCampfireが自身のテストスイートを全件通過しました。ただしroundhouseはSpinel本体ではなくコミュニティ製です。
既存gemについては、spinelgemsがSpinelでコンパイルできるgemを調査しており、Gemfileの依存をベンダリングして互換性を確かめるBundlerプラグインbundler-spinelも同じ場所で公開されています。コードがSpinelで書けない構文を使っていないかは、RuboCopのカスタムCopであるrubocop_spinelで事前に検出できます。RuboCop本体の設定方法はRuboCopの使い方と設定を参照してください。
よくある質問
SpinelとYJIT・ZJITは何が違いますか?
YJITとZJITはCRubyに組み込まれたJITコンパイラで、実行中に頻繁に通るコードを機械語にします。Rubyのコードはそのまま動きますが、実行にはRuby本体が必要です。SpinelはCRubyとは別のコンパイラで、実行前にプログラム全体をCに変換し、Ruby不要のバイナリを作ります。Ruby 4.0でのZJITの位置づけはRubyの最新バージョン4.0.7の解説にまとめています。
Spinelはgem installで導入できますか?
できません。READMEはgem install spinelは存在しないと明記しています。GitHubのmatz/spinelをcloneしてmake depsとmakeを実行するか、リリースのソースアーカイブspinel-2026.09.12.tar.xzを展開してmakeします。
Windowsで使えますか?
READMEはWSLでの利用を推奨しています。WSL上ではLinuxと同じ手順でビルドして動かせます。MinGW-w64 gccを使うネイティブのWindows版もありますが、コミュニティによるベストエフォートで、masterより遅れることがあると説明されています。
Ruby on Railsのアプリをコンパイルできますか?
直接はできません。Railsはメタプログラミングに強く依存しているためです。コミュニティ製のroundhouseでRailsアプリを素のRubyへ変換すればコンパイルできる場合があり、READMEはCampfireがテストスイートを全件通過した例を挙げています。
MatzのSpinelのGitHubリポジトリはどこですか?
github.com/matz/spinelです。ライセンスはMITで、2026年10月8日時点のスター数は2,207です。不具合の報告には、同じプログラムをCRubyとSpinelで実行して差分を出すspinel diffの結果を貼るよう、READMEの貢献ルールが求めています。