
2026/04/24 17:28
スパインル:Ruby AOT ネイティブコンパイラ
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Spinel は、標準的な Ruby コードを外部のランタイムライブラリ(標準の C/C 数学ライブラリ以外を除く)を必要とせずに、スタンドアロンの高パフォーマンスなネイティブバイナリに変換する画期的な新しいコンパイラです。全プログラムの型推論と積極的な最適化(小規模クラスへのスタック割り当て、文字列結合チェーンの平坦化による割付削減、静的シンボルインターニング等)を活用することで、劇的な高速化を実現します。ベンチマークの結果では、miniruby 대비 幾何平均で 11.6 倍の実行速度向上を示し、「コンウェイのゲーム・オブ・ライフ」などの特定タスクでは最大 86.7 倍の改善が確認されています。重要な点は、このコンパイラは自己ホスト化されており、メタプログラミングや eval を使用せずに特別な Ruby サブセットを利用して自身をコンパイルできることであり、並行処理や任意精度算術を含む 74 のコア機能テストで堅牢性を保っています。この技術により、開発者は Ruby アプリケーションを単一バイナリとして配信し、システムに標準 C ライブラリがあれば効率的に実行でき、企業_grade のソリューションを展開する企業が重いランタイム環境のインストールや複数の依存関係の管理といった複雑さを排除できます。
本文
Spinel -- Ruby AOT コンパイラ
Spinel は、Ruby ソースコードを独自の実行ファイル(ネイティブバイナリ)へとコンパイルするツールです。プログラム全体を対象とした型推論を実装し、最適化された C コードを生成することで、CRuby に比べて大幅な速度向上を実現しています。Spinel は自身もコンパイル対象となる自己完結型の設計を採用しており、コンパイラのバックエンドは Ruby で記述され、それ自身がネイティブバイナリとしてコンパイルされます。
仕組みについて
Ruby (.rb) | v spinel_parse Prism (libprism) を用いたパーサーで解析し、AST をシリアライズする | (C バイナリを使用するか、CRuby + Prism ジェムをフォールバックとして使用する) v AST テキストファイル | v spinel_codegen 型推論と C コードの生成を行う(自己完結型のネイティブバイナリ) v C ソース (.c) | v cc -O2 -Ilib -lm 標準的な C コンパイラー + ランタイムヘッダーを使用 | v ネイティブバイナリ インストール不要(ランタイム依存関係なし)の自立型実行ファイル
クイックスタート
libprism のソースを取得する (rubygems.org の prism ジェムから):
make deps
全ビルトを実行:
make
Ruby プログラムを作成:
cat > hello.rb <<'RUBY' def fib(n) if n < 2 n else fib(n - 1) + fib(n - 2) end end
puts fib(34) RUBY
コンパイルして実行:
./spinel hello.rb ./hello # 値として「5702887」を出力(瞬時に完了)
オプション
# ./app という名前でコンパイル./spinel app.rb
# ./myapp という名前でコンパイル./spinel app.rb -o myapp
# 生成処理のみを行い、app.c を出力./spinel app.rb -c
# C コードを標準出力 (stdout) に表示./spinel app.rb -S
自己完結化 (Self-Hosting)
Spinel は自身のバックエンドをコンパイルします。ブートストラップチェーンの工程は以下の通りです:
- CRuby + spinel_parse.rb → AST
- CRuby + spinel_codegen.rb → gen1.c → bin1
- bin1 + AST → gen2.c → bin2
- bin2 + AST → gen3.c
(ブートストラップループが閉じる)gen2.c == gen3.c
ベンチマーク結果
74 の機能テストが合格し、55 のベンチマークが合格しました。 幾何平均では、以下の 28 つのベンチマークにおいて miniruby (Ruby 4.1.0dev) よりも約 11.6 倍高速です。基線となるのはバンドルされたジェムなしの最新 CRuby miniruby ビルドで、これはシステム上の Ruby (3.2.3) を大幅に上回ります。Spinel の優位性は相対的に小さくなるものの、計算量が多い負荷においては依然として実用的なパフォーマンスです。
計算密集型
| ベンチマーク | Spinel | miniruby | スピードアップ |
|---|---|---|---|
| life (コンウェイのライフゲーム) | 20 ms | 1_733 ms | 86.7 倍 |
| ackermann | 5 ms | 374 ms | 74.8 倍 |
| mandelbrot | 25 ms | 1_453 ms | 58.1 倍 |
| fib (再帰的) | 17 ms | 581 ms | 34.2 倍 |
| nqueens | 10 ms | 304 ms | 30.4 倍 |
| tarai | 16 ms | 461 ms | 28.8 倍 |
| tak | 22 ms | 532 ms | 24.2 倍 |
| matmul | 13 ms | 313 ms | 24.1 倍 |
| sudoku | 6 ms | 102 ms | 17.0 倍 |
| partial_sums | 93 ms | 1_498 ms | 16.1 倍 |
| fannkuch | 2 ms | 19 ms | 9.5 倍 |
| sieve | 39 ms | 332 ms | 8.5 倍 |
| fasta (DNA シーケンス生成) | 3 ms | 21 ms | 7.0 倍 |
データ構造と GC
| ベンチマーク | Spinel | miniruby | スピードアップ |
|---|---|---|---|
| rbtree (Red-Black ツリー) | 24 ms | 543 ms | 22.6 倍 |
| splay tree | 14 ms | 195 ms | 13.9 倍 |
| huffman (符号化) | 6 ms | 59 ms | 9.8 倍 |
| so_lists | 76 ms | 410 ms | 5.4 倍 |
| binary_trees | 11 ms | 40 ms | 3.6 倍 |
| linked_list | 136 ms | 388 ms | 2.9 倍 |
| gcbench | 1_845 ms | 3_641 ms | 2.0 倍 |
リアルワールドアプリ
| ベンチマーク | Spinel | miniruby | スピードアップ |
|---|---|---|---|
| json_parse | 39 ms | 394 ms | 10.1 倍 |
| bigint_fib (桁数 1,000) | 2 ms | 16 ms | 8.0 倍 |
| ao_render (レイトレーサー) | 417 ms | 3_334 ms | 8.0 倍 |
| pidigits (大整数計算) | 2 ms | 13 ms | 6.5 倍 |
| str_concat | 2 ms | 13 ms | 6.5 倍 |
| template engine | 152 ms | 936 ms | 6.2 倍 |
| csv_process | 234 ms | 860 ms | 3.7 倍 |
| io_wordcount | 33 ms | 97 ms | 2.9 倍 |
サポートされている Ruby 機能
- コア: クラス、継承、super, include (ミクシン), attr_accessor, Struct.new, alias, モジュール定数、組み込み型のオープンクラス。
- 制御フロー: if/elsif/else, unless, case/when, case/in (パターンマッチング), while, until, loop, for..in (レンジと配列), break, next, return, catch/throw, &. (セーフナビゲーション)。
- ブロック: yield, block_given?, &block, proc {}, Proc.new, lambda -> x { }, method(:name)。ブロックメソッド: each, each_with_index, map, select, reject, reduce, sort_by, any?, all?, none?, times, upto, downto。
- 例外処理: begin/rescue/ensure/retry, raise, 独自に定義した例外クラス。
- 型: Integer, Float, String (不変+可変), Array, Hash, Range, Time, StringIO, File, Regexp, Bigint (自動昇格), Fiber。タグ付きユニオンによる多型値。自己参照的なデータ構造向けに nullable object types (T?) を提供。
- グローバル変数:
は静的な C 変数へとコンパイルされ、コンパイル時に型不一致を検出します。$name - 文字列:
オペレーターは自動的に可変文字列 (sp_String) に昇格し、O(n) インプレイスアプレンドを実現。<<
, インポーレーション, tr, ljust/rjust/center および標準的なメソッドは両方の型で動作します。文字比較+
はゼロ割り当てによる直接の文字配列アクセスへと最適化されます。連鎖結合 (s[i] == "c"
) は単一の malloc 呼び出し (sp_str_concat4 / sp_str_concat_arr) に集約され、N-1 つの中間文字列を削減します。ループ内のa + b + c + d
は既存の sp_StrArray を再利用するため(csv_process: 400 万回の割り当てを削除)、配分のオーバーヘッドが軽減されます。str.split(sep) - Regexp: 組み込みの NFA 型正則表現式エンジン(外部依存関係なし)。
,=~
,$1-$9
,match?
,gsub(/re/, str)
,sub(/re/, str)
,scan(/re/)
がサポートされています。split(/re/) - Bigint: mruby-bigint を用いた任意精度整数。ループ内の乗算パターン(例:
)やフィボナッチ数列スタイルの自己参照的な加算(q = q * k
)から自動的に昇格されます。静的ライブラリとしてリンクされ、使用時にのみ読み込まれます。c = a + b - Fiber: ucontext_t を用いた協調型並行性の実装。Fiber.new, Fiber#resume, 値渡り付きの Fiber.yield がサポートされています。ヒープ上で昇格されたセルを介して自由変数をキャプチャします。
- メモリ管理: サイズセグメント化されたフリーリスト、非再帰的なマーキング、スティッキーマークビットを採用したマーク&スウィープ GC。小さいクラス(8 個以下のスカラーフィールド、継承なし、パラメータ経由での変異なし)は自動的にスタック配分されたバリュータイプとして処理され、5 フィールドのクラスの 100 万回の割り当てが 85 ms から 2 ms に短縮されます。バリュータイプのみのプログラムでは GC ランタイムを一切発症させません。
- シンボル: 独自の sp_sym タイプであり、文字列とは異なる(
)。シンボルリテラルはコンパイル時に SP S_name 定数へインターン化され、文字列#to_sym は動的プールが実際に必要な場合にのみ使用されます。シンボルキー付きのハッシュ:a != "a"
は、sp_SymIntHash を用いて sp_sym (整数) キーを直接格納するため、strcmp や動的な文字列割り当てを行いません。{a: 1} - I/O: puts, print, printf, p, gets, ARGV, ENV[], File.read/write/open (ブロック付き), system(), 反引字表現。
最適化技術
プログラム全体を対象とした型推論に基づいて、コンパイル時において以下のような最適化が施されます:
- バリュータイプの昇格: 小さな不変クラス(8 個以下のスカラーフィールド)は C struct としてスタック上での処理となり、GC オーバーヘッドを完全に排除します。
- 定数伝播: 単純なリテラル定数(N = 100 など)は使用場所でインライン展開され、cst_N ランタイムルックアップを回避します。
- ループ不変長の引き上げ:
では、ループ開始前に arr.length が一度評価されます。「while i < arr.length
」の場合は strlen が引き上げられます。本体内でリシーバーの修飾(例:while i < str.length
)がある場合は、引き上げが無効化されます。arr.push - メソッドのインライン展開: 短いメソッド(3 語句以下かつ非再帰)は static inline にされ、gcc がコールサイトでインライン展開できます。
- 文字列結合チェーンのフラッティング:
は単一の sp_str_concat4 / sp_str_concat_arr 呼び出しにコンパイルされ、1 つの malloc で N-1 つの中間文字列を削減します。a + b + c + d - Bigint の自動昇格:
のようなループやフィボナッチ数列スタイルの自己参照的な加算x = x * y
は自動的に Bigint として処理されます。c = a + b - Bigint に変換する to_s: mruby-bigint の mpz_get_str を用いた分割統治法 O(n log²n) で、 naive な O(n²) より高速です。
- 静的シンボルインターン化:
はコンパイル時間の SPS_ 定数に解決され、動的なプールは実際の動的インターンが使用された場合にのみ出力されます。"literal".to_sym - sub_range 内の strlen キャッシャーリング: 文字列の長さが引き上げられた場合、
アクセスでは内部の strlen 呼び出しをスキップするために sp_str_sub_range_len を使用します。str[i] - split の再利用: ループ内の
は新しい配列を割り当てるのではなく、既存の sp_StrArray を再利用します。fields = line.split(",") - デッドコードエリミネーション:
でコンパイルされ、-ffunction-sections -fdata-sections
でリンカー処理されるため、使用されていないランタイム関数が最終バイナリから除去されます。--gc-sections - 早期終了を促す推論ループ: param/return/ivar 固定点ループは、3 つの配列の署名が変更しない時点で即座に停止します。大半のプログラムは全工程である 4 回ではなく、1〜2 回の反復で収束し、ブートストラップ時間を約 14% 短縮します。
- parse_id_list のバイトウォーク: AST フィールドリストパーサー(自己コンパイル時に約 12 万回呼び出される)は、
ではなくs.split(",")
を用いて手動でバイトをウォークすることで、N+1 つの割り当てを毎回 2 個に削減します。s.bytes[i] - 警告のないビルト: テストおよびベンチマークの全てでデフォルトの警告レベルにおいてきれいにコンパイルされるように生成された C コードは設計されています。ハネスでは
を使用するため、回退が即座に表面化します。-Werror
アーキテクチャ
- spinel 1 つのコマンドで動作するラッパースクリプト (POSIX シェル)
- spinel_parse.c C フロントエンド: libprism → テキスト AST(約 1,061 行)
- spinel_codegen.rb コンパイラバックエンド: AST → C コード(約 21,109 行)
- lib/sp_runtime.h ランタイムライブラリヘッダー(581 行)
- lib/sp_bigint.c 任意精度整数(5,394 行)
- lib/regexp/ 組み込みの正則表現式エンジン(1,759 行)
- test/ 機能テスト 74 テスト分
- benchmark/ ベンチマーク 55 件
- Makefile ビルド自動化
コンパイラバックエンド (spinel_codegen.rb) は、Spinel 自身がコンパイル可能な Ruby のサブセットで記述されています。サポートされる構文にはクラス定義、def, attr_accessor, if/case/while, each/map/select, yield, begin/rescue、文字列/配列/ハッシュの操作、および File I/O が含まれます。メタプログラミング、eval、バックエンドでの require は使用されません。
ランタイム (lib/sp_runtime.h) には GC、配列/ハッシュ/文字列の実装、およびすべてのランタイムサポート機能が単一のヘッダーファイルに集約されています。生成された C コードはこのヘッダーを含め、リンカーは libspinel_rt.a(bigint と正則表現式エンジン)から必要な部分だけを参照します。
パーサーには 2 つの実装があります:
- spinel_parse.c libprism を直接リンク(CRuby は不要)
- spinel_parse.rb Prism ジェムを使用(CRuby のフォールバック)
両方の実装は同等の AST 出力を生成します。Spinel のラッパーでは、C バイナリが利用可能である場合はそれを優先的に使用します。require_relative はパーサ実行時に参照ファイルをインライン展開することで解決されます。
ビルド方法
# vendor/prism に libprism を取得する(初回のみ)make deps
# パーサー、regexp ライブラリ、ブートストラップコンパイラをビルドmake
# 74 の機能テストを実行(ブートストラップが必要)make test
# 55 のベンチマークを実行(ブートストラップが必要)make bench
# ソースからコンパイラを再構築make bootstrap
# /usr/local にインストール (PATH に spinel を追加)sudo make install
# ビルドアーティファクトを削除make clean
インストール先ディレクトリを上書きするには:
make install PREFIX=$HOME/.local
Prism は Spinel のパーサーとして使用されます。
make deps コマンドは rubygems.org から prism ジェムの tarball をダウンロードし、C ソースを vendor/prism に展開します。すでに Prism ジェムがインストールされている場合はビルトが自動的に検出され、必要であれば PRISM_DIR=/path/to/prism によってカスタムディレクトリを指定することもできます。
CRuby は初期のブートストラップ用だけに必要です。
make コマンドを実行後には、その後のパイプライン全体は Ruby を使用せずに動作します。
制限事項
- eval のサポートなし: eval, instance_eval, class_eval
- メタプログラミングのサポートなし: send, method_missing, define_method (動的)
- スレッドのサポートなし: Thread, Mutex(Fiber はサポート)
- エンコーディングのサポートなし: UTF-8/ASCII を仮定
- 一般化されたラムダ演算子のサポートなし: [] 呼び出し付きの深いネスト
-> x { }
依存関係
- ビルド時: libprism (C ライブラリ)、CRuby(ブートストラップ用のみ)
- 実行時: なし。生成されたバイナリは libc と libm のみが必要です。
- Regexp: 組み込みエンジンを使用するため、外部ライブラリは不要です。
- Bigint: mruby-bigint から導出された組み込み機能であり、使用時にのみリンクされます。
歴史
Spinel は当初 C (約 1.8 万行目の c-version ブランチ) で実装され、その後 Ruby (ruby-v1 ブランチ) に書き換えられ、最後に自己完結型の Ruby サブセット(現在の master ブランチ)に書き換えられました。
ライセンス
MIT ライセンス。LICENSE ファイルを参照してください。