1 ポイント 投稿者 GN⁺ 5 일 전 | 1件のコメント | WhatsAppで共有
  • Rubyコードをスタンドアロンのネイティブバイナリに変換し、プログラム全体単位の型推論とCコード生成によって最新のCRuby miniruby と比べて幾何平均で約11.6倍高速な実行を目指す
  • コンパイルパイプラインはPrismベースのパーサでRubyをASTテキストに変換した後、self-hostingバックエンドが型推論とCコード生成を行い、標準Cコンパイラでスタンドアロンバイナリを生成する
  • コンパイラバックエンドはRubyで書かれたself-hosting構造を持ち、ブートストラップ過程を経て gen2.c == gen3.c が成立し、自分自身を再コンパイルするループが閉じる
  • 文字列連結の平坦化、value-type promotion、loop-invariant length hoisting、static symbol interning、bigint自動昇格といったコンパイル時最適化を備え、内蔵regexpエンジンとbigint、単一ヘッダーのランタイムによって外部ランタイム依存を減らす
  • eval、メタプログラミング、Thread、一般的なエンコーディング処理はサポートしないが、Rubyなしで実行できる配布形態と計算集約型ワークロードでの大きな性能差により、Ruby AOTコンパイルの実用性が示されている

仕組み

  • コンパイルパイプラインはRubyファイルをパースしてASTテキストファイルへシリアライズした後、型推論とCコード生成を経て標準Cコンパイラでネイティブバイナリを作る流れで構成される
  • spinel_parse はPrismとlibprismを使ってRubyをパースし、Cバイナリがない場合はCRubyとPrism gemを使うフォールバック経路を使用する
  • spinel_codegen はself-hostedなネイティブバイナリとして動作し、ASTを受け取って型推論 + Cコード生成を行う
  • 最終段階では cc -O2 -Ilib -lm でCソースとランタイムヘッダを一緒にコンパイルし、生成されるバイナリはstandaloneな形になる

Self-Hosting

  • ブートストラップチェーンは CRuby + spinel_parse.rb でASTを作り、CRuby + spinel_codegen.rbgen1.cbin1 を作成し、その後生成されたバイナリで再び gen2.cgen3.c を作る方式で閉じる
  • gen2.c == gen3.c が成立し、bootstrap loop が閉じたことを示している
  • バックエンドである spinel_codegen.rb は、Spinel自身でコンパイル可能なRubyサブセットで書かれている
    • classes、defattr_accessor
    • if/case/while
    • each/map/selectyield
    • begin/rescue
    • String、Array、Hash演算とFile I/O
  • バックエンドにはmetaprogrammingevalrequire を入れていない

性能とベンチマーク

  • テストは74件通過、ベンチマークは55件通過の状態
  • 28件のベンチマーク基準で、幾何平均は最新のCRuby miniruby と比べて約11.6倍高速
  • 比較対象はバンドルgemなしの最新CRuby miniruby ビルドであり、システム ruby 3.2.3よりも高速な基準と比較しても、計算集約型ワークロードで大きな優位性を持つ
  • 計算性能

    • life は20ms対1,733msで86.7倍高速
    • ackermann は5ms対374msで74.8倍高速
    • mandelbrot は25ms対1,453msで58.1倍高速
    • fib 再帰版は17ms対581msで34.2倍高速
    • nqueens は10ms対304msで30.4倍高速
    • tarai は16ms対461msで28.8倍高速
    • tak は22ms対532msで24.2倍高速
    • matmul は13ms対313msで24.1倍高速
    • sudoku は6ms対102msで17.0倍高速
    • partial_sums は93ms対1,498msで16.1倍高速
    • fannkuch は2ms対19msで9.5倍高速
    • sieve は39ms対332msで8.5倍高速
    • fasta は3ms対21msで7.0倍高速
  • データ構造とGC

    • rbtree は24ms対543msで22.6倍高速
    • splay tree は14ms対195msで13.9倍高速
    • huffman は6ms対59msで9.8倍高速
    • so_lists は76ms対410msで5.4倍高速
    • binary_trees は11ms対40msで3.6倍高速
    • linked_list は136ms対388msで2.9倍高速
    • gcbench は1,845ms対3,641msで2.0倍高速
  • 実アプリケーション

    • json_parse は39ms対394msで10.1倍高速
    • bigint_fib の1000桁計算は2ms対16msで8.0倍高速
    • ao_render は417ms対3,334msで8.0倍高速
    • pidigits は2ms対13msで6.5倍高速
    • str_concat は2ms対13msで6.5倍高速
    • template engine は152ms対936msで6.2倍高速
    • csv_process は234ms対860msで3.7倍高速
    • io_wordcount は33ms対97msで2.9倍高速

サポートするRuby機能

  • Core 機能として、classes、inheritance、superinclude ミックスイン、attr_accessorStruct.newalias、module constants、組み込み型に対するopen classesをサポートする
  • Control Flow として、if/elsif/elseunlesscase/whencase/in パターンマッチ、whileuntilloopfor..inbreaknextreturncatch/throw&. をサポートする
  • Blocks として、yieldblock_given?&blockproc {}Proc.new-> x { }method(:name) をサポートし、eachmapselectreducesort_bytimesuptodownto などのブロックメソッドも含む
  • Exceptions として、begin/rescue/ensure/retryraise、ユーザー定義例外クラスをサポートする
  • Types には Integer、Float、String、Array、Hash、Range、Time、StringIO、File、Regexp、Bigint、Fiber を含む
    • 多相な値はtagged unionsとして処理する
    • 自己参照データ構造のために nullable object types T? を用意する
  • Global Variables$name を静的C変数としてコンパイルし、型不一致はコンパイル時に検出する
  • I/OputsprintprintfpgetsARGVENV[]File.read/write/opensystem()、バッククォートをサポートする

文字列、正規表現、シンボル、Bigint、Fiber

  • Strings は不変文字列と可変文字列の両方を扱い、<< は自動的に可変文字列 sp_String に昇格されて O(n) のインプレースappendを行う
  • +、interpolation、trljust/rjust/center と標準メソッドは両方の文字列表現で動作する
  • s[i] == "c" のような比較はchar配列へ直接アクセスするよう最適化され、割り当てなしで処理される
  • a + b + c + d のような連結は sp_str_concat4 または sp_str_concat_arr 1回に平坦化され、N-1個少ない割り当てに変わる
  • ループ内の str.split(sep) は同じ sp_StrArray を繰り返し再利用し、csv_process では400万件の割り当て削減が発生する
  • Regexp は外部依存のない内蔵NFA regexp engineを使用する
    • =~$1-$9match?gsubsubscansplit をサポートする
  • Bigint はmruby-bigintベースの任意精度整数を使用する
    • q = q * k のようなループ乗算パターンで自動昇格される
    • 静的ライブラリとしてリンクされ、実際に使用するときだけ含まれる
  • Fiberucontext_t ベースの協調的並行性を提供する
    • Fiber.newFiber#resumeFiber.yield と値の受け渡しをサポートする
    • 自由変数はheap-promoted cellsとしてキャプチャされる
  • Symbols は文字列と分離された sp_sym 型として実装される
    • :a != "a" を維持する
    • シンボルリテラルはコンパイル時にinternされ、SPS_name 定数になる
    • String#to_sym は必要なときだけ動的プールを使う
    • シンボルキーのハッシュは sp_SymIntHash を使い、文字列ではなく整数キーを直接保存するため、strcmpと動的文字列割り当てがなくなる

メモリ管理と値型

  • メモリ管理にはmark-and-sweep GCを使用し、size-segregated free lists、non-recursive marking、sticky mark bitsを含む
  • 小さく単純なクラスは自動的にvalue typesへ昇格し、スタック上に配置される
    • 条件はスカラーfieldが8個以下
    • 継承なし
    • パラメータ経由の変更なし
  • fieldが5個のクラスを100万回割り当てるケースでは、85msから2msへ短縮される
  • 値型のみを使うプログラムでは、GCランタイム自体がまったく出力されない

最適化

  • プログラム全体単位の型推論に基づいて、さまざまなコンパイル時最適化を行う
  • Value-type promotion により、小さく不変なクラスはC structのスタックオブジェクトとなり、GCオーバーヘッドをなくす
  • Constant propagation により、N = 100 のような単純なリテラル定数は cst_N 参照を介さず利用箇所へ直接インラインされる
  • Loop-invariant length hoisting により、while i < arr.length はループ前に長さを1回だけ計算する
    • 本文で arr.push のようにレシーバオブジェクトを変更すると、このhoistは無効化される
  • Method inlining は短く非再帰な3文以下のメソッドに static inline を付けてgccのインライン化を促す
  • String concat chain flattening は連結チェーンを単一呼び出しに減らし、中間文字列生成をなくす
  • Bigint auto-promotion は自己参照加算や反復乗算パターンをbigintに自動昇格する
  • Bigint to_s はmruby-bigintの mpz_get_str を使い、divide-and-conquerの**O(n log²n)**で処理する
  • Static symbol interning"literal".to_sym をコンパイル時の SPS_<name> 定数に変換し、動的interningを使う場合にのみランタイムプールを含める
  • sub_range で長さがhoistされた文字列は sp_str_sub_range_len を使い、内部の strlen 呼び出しを省く
  • ループ内部の line.split(",") は既存の sp_StrArray を再利用する
  • Dead-code elimination-ffunction-sections -fdata-sections--gc-sections を使い、未使用のランタイム関数を最終バイナリから削除する
  • Iterative inference early exit は、param、return、ivarの3つのシグネチャ配列がそれ以上変化しなくなった時点で固定点ループを即座に停止する
    • ほとんどのプログラムは4回の全反復ではなく1〜2回で収束する
    • bootstrap時間は約**14%**短縮される
  • parse_id_list byte walk はself-compile中に約12万回呼ばれるASTフィールドリストパーサを、s.split(",") の代わりに s.bytes[i] の手動走査へ置き換え、呼び出しごとの割り当てをN+1個から2個へ減らす
  • 生成されるCコードは標準的な警告レベルでwarning-free buildを維持し、ハーネスでは -Werror を使って回帰を即座に表面化させる

アーキテクチャ

  • リポジトリ構成は次の要素に分かれる
    • spinel: POSIX shellベースのワンコマンドラッパースクリプト
    • spinel_parse.c: libprismからテキストASTへ変換するCフロントエンド 1,061行
    • spinel_codegen.rb: ASTからCコードへ変換するコンパイラバックエンド 21,109行
    • lib/sp_runtime.h: ランタイムライブラリヘッダ 581行
    • lib/sp_bigint.c: 任意精度整数 5,394行
    • lib/regexp/: 内蔵regexpエンジン 1,759行
    • test/: 機能テスト 74件
    • benchmark/: ベンチマーク 55件
    • Makefile: ビルド自動化
  • ランタイム lib/sp_runtime.h はGC、array/hash/string実装とその他のランタイム支援を1つのヘッダファイルに収めている
  • 生成されたCコードはこのヘッダをincludeし、リンカは libspinel_rt.a から必要な部分だけを取り込む
    • bigint
    • regexp engine
  • パーサには2つの実装がある
    • spinel_parse.c はlibprismを直接リンクし、CRubyなしで動作する
    • spinel_parse.rb はPrism gemを使うCRubyフォールバックである
  • 両方のパーサは同じAST出力を生成し、spinel ラッパーは可能な限りCバイナリを優先して使う
  • require_relative はパース時点で解決され、参照されたファイルがインライン展開される

制約事項

  • No eval: evalinstance_evalclass_eval はサポートしない
  • No metaprogramming: sendmethod_missing、動的 define_method はサポートしない
  • No threads: ThreadMutex はサポートせず、Fiberのみをサポートする
  • No encoding: UTF-8とASCIIを前提とする
  • No general lambda calculus: 深くネストした -> x { }[] 呼び出しは扱わない

依存関係と実行モデル

  • ビルド時の依存関係は libprism Cライブラリと、初期ブートストラップ用のCRuby
  • ランタイム依存はなく、生成されたバイナリはlibc + libmのみを必要とする
  • 正規表現は内蔵エンジンを使うため外部ライブラリは不要
  • Bigintは内蔵されているが、実際に使うときだけリンクされる
  • Prismspinel_parse が使用するRubyパーサ
    • make deps はrubygems.orgのprism gem tarballをダウンロードし、Cソースを vendor/prism に展開する
    • prism gemがすでにインストールされていれば自動検出する
    • PRISM_DIR=/path/to/prism でユーザー指定のパスを使うこともできる
  • CRubyは初期bootstrapにのみ必要で、make 後はパイプライン全体がRubyなしで実行される

プロジェクト履歴

  • Spinelは当初Cで実装され、規模は18K linesで、c-version ブランチに残っている
  • その後、Rubyで書き直した ruby-v1 ブランチを経た
  • 現在の master は、self-hosting可能なRubyサブセットで再実装されたバージョン

ライセンス

  • MIT Licenseを使用
  • LICENSE ファイルに従う

1件のコメント

 
GN⁺ 5 일 전
Hacker Newsのコメント
  • Matzが作ったものなら、Ruby semanticsの限界もよく分かっているはずなので信頼できる
    私の修士論文も AOT JS compiler だったが、動きはしたものの入力データの制約が大きく、結局やめた
    当時のJS開発者は自発的に制約をきちんと守ることにあまり慣れておらず、JSON.parseのように本質的に分からない入力が障害になった
    今ならTypeScriptのおかげで、当時よりずっと現実的かもしれない
    一般的な lambda calculus を見ても型推論の限界は明らかで、Matt Might周辺の論文やShed-skin Pythonの取り組みでも似たような制約が見えてくる
    eval, send, method_missing, define_method が実際のRubyコードでどれほど一般的なのか気になるし、型なしのパース、たとえばJSON入力を普通どう扱うのかも気になる

    • この設計はかなり pragmatic に見える
      Rubyのパースは変換そのものより難しいくらいなので Prism を使い、結果としてCを生成する
      基本的なRuby semantics自体の実装は、そこまで難しくない
      一方で私は純Ruby製の古い self-hosting AOT compiler を抱えていて、独自パーサーにこだわったせいでわざわざずっと険しい道を進んでしまった
      初期の80%はざっくり作ってもかなりの量のRubyコードが動くことを早い段階で学んだが、本当に難しい「2つ目の80%」は、Matzが今回のプロジェクトやmrubyで外した部分、たとえばエンコーディングやありとあらゆる周辺機能に集中している
      正直、Rubyには実際のコードで一度も見たことのない機能もかなりあるので、いくつかはdeprecatedになっても不思議ではないと思う
      send, method_missing, define_method はとてもよく使われる
      制約はmrubyに近く、その制約下でも使い道はある
      send, method_missing, define_method のサポートは比較的容易だ
      反対に eval() のサポートはとてつもなくつらい
      ただしRubyにおける eval() のかなりの割合は、静的に instance_evalのblock版 に還元できるので、その場合はAOTコンパイルがかなりやりやすくなる
      たとえば eval() に入る文字列を静的に把握できる、あるいは分解できるなら、解決の余地は大きい
      実際には多くの eval() 利用は不要だったり、単純なintrospection回避に近かったりするので、静的検査で処理できる
      私のコンパイラでも、そこがボトルネックになったらまずそこから手を付けるつもりだ
    • こうした機能がかなり多くないと Rails流のmagic は作れない
      型なしのJSON ingestionも、おそらくそうした仕組みを使っている可能性が高い
      それを取り除けば、Crystalほど強い型付けではないが、公式Rubyほどメタプログラミングに依存もしない、小さくて読みやすい言語が残る
      なので潜在力はかなり大きそうだが、結局は時間が経たないと判断できない
    • Rubyを Objective-C にコンパイルする方式なら、Rubyの機能をすべてサポートしつつ、インタプリタRubyより高速にできそうだ
    • 私は eval をよく使う側だ
      使わずに済ませることもできるが、私にとってはそのほうがより ergonomic だ
    • 私の経験で興味深いのは、eval, exec, define_method、そして Class.new, Struct.new で新しいクラスを作るパターンだ
      これらの利用の大半はアプリの boot時点 やファイルのrequire中に集中していて、ある意味ではすでにコンパイル段階に近い
  • これは RubyKaigi 2026 でMatzがたった今発表したものだ
    実験的ではあるが、Claudeの助けを借りて約1か月で作られ、ライブデモも成功した
    名前はMatzの新しい猫に由来し、その猫の名前はCard Captor Sakuraの猫の名前から来ていて、そこからさらにRubyという名前のキャラクターと対になる

    • AIがプログラムを最初から最後まで全部作るという話はよくあるが、より現実的なシナリオは 10x programmer100x programmer にすることだと思う
      Matzのような人にとっては、100xを500xまで押し上げることになるのかもしれない
    • 私の頭の中で最新のSpinelはSteven Universeのほうなので、Spinel/Ruby (Moon) pun はまったく分からなかったが、知ってから一日が楽しくなった
    • 私は当然 鉱物のspinel の話だと思っていた :)
      https://en.wikipedia.org/wiki/Spinel
    • ありがとう
      動画はまだライブではないようで、こちらのチャンネルに1本ずつ上がっているようだ
      https://www.youtube.com/@rubykaigi4884/videos
    • その猫の名前の由来の話は、Ruby Central drama やSpinel.coop創業者たちとの関係を考えると、かなり意味深に見える
      プロジェクト名も感情的に付けられたように感じる
  • 間違いなく非常に印象的だが、AI agent なしでは保守不可能に見える
    spinel_codegen.rb は2万1千行あり、あるメソッドはネストが15段階に達している
    もともとコンパイラのコードはきれいになりにくいが、それを差し引いてもこれは人間が管理するにはかなり厳しそうだ

    • コンパイラのコードは、時間さえあれば十分 きれいに できる
      コンパイラはサブシステムの境界が明確で、段階ごとのhandoffもはっきりしているので、むしろ最もmodularに作りやすい部類だ
      問題はたいてい、まず動かすことを優先してリファクタリングする時間がなく、その結果として汚さが膨らみ続けることにある
    • spinel_codegen.rb はほとんど eldritch horror レベルだ
      Claudeを使うと私もいつもこういうスパゲティコードになってしまうので、自分が何か間違っているのかと思っていた
      でも、最高レベルのプログラマだと思っている人が作った本当に面白いプロジェクトでも、コード品質がところどころかなり悪いのを見て、自分だけではなかったのだと分かった
      たとえば infer_comparison_type() は最悪の例というほどではなく、読みにくくもないが、もっと単純で明快な実装があるのにClaudeはそこへ行けない
      Set に比較演算子をまとめて include? で処理すれば、もっと短く、速く、読みやすく、保守もしやすい
      なのにClaudeはいつも if-returnの連鎖 に流れ、if-elseすら苦手な感じがある
      私のClaudeコードベースもそういうパターンだらけだが、もう自分だけではないと分かった
      一方で他のファイルはかなり良く、とくに lib ディレクトリはメインのRubyリポジトリの ext ディレクトリに対応しているようで、品質も悪くない
      APIもMRI Rubyの影響を明確に受けており、実装はかなり違っていても、Matzが元のAPIに似せるよう誘導したことで出力がより整っているように見える
      [1] https://github.com/matz/spinel/blob/98d1179670e4d6486bbd1547...
    • 今の段階では、人が手で保守できるかどうかはそこまで重要ではないと思う
      テストとベンチマークさえ通れば、ひとまず満足だ
      ただ、巨大なファイルがAIにとっても扱いやすいのかは疑問だ
      私はファイルを300行以内に抑えるようにしていて、人が理解しやすいコードは coding agents にとっても扱いやすいはずだと思っている
  • 制約はこうらしい
    No eval: eval, instance_eval, class_eval
    No metaprogramming: send, method_missing, define_method(動的)
    No threads: Thread, Mutex(Fiberはサポート)
    No encoding: UTF-8/ASCII前提
    No general lambda calculus: [] 呼び出しを伴う深いネストの -> x { }
    UTF-8/ASCII前提は個人的には大きな制約ではないが、残りはかなり多くのプログラムで実際に制約になりそうだ
    そして、これを再び入れるにはかなりの作業が必要に見える

    • これだとRubyの magic のかなりの部分が消える
  • Rubyを長く使ってきて、列挙された機能を全部使ったことのある立場からすると、むしろ進化の果てに自分が欲しくなったのは、こういう シンプルなRuby
    よりシンプルで理解しやすいのに、Ruby特有の美意識は残っている
    今はLLMのおかげでコード生成の生産性があまりに高く、昔のように開発者の生産性のためにメタプログラミングでboilerplateを減らす必要が薄れてきている
    開発者が自分でコードを書く比重そのものが下がっているからだ

    • 求めているのが単に Ruby aesthetic なら、Crystalもかなり合うかもしれない
      文法は似ていて静的型システムがあり、より効率的なコンパイル済みコードにつながる
    • eval がないのはむしろ良いと思うが、threadsとmutexes までないのは惜しい
      define_method がないのは用途を考えれば理解できる
      ただ sendmethod_missing は既存ライブラリでよく使われるし、実装もメモリ上のlookup tableをコンパイル時に構築するような形で、そこまで難しくなさそうに思える
      なので意図的に外したのか、まだそこまで到達していないのか分からない
      願わくは後者だが、少なくとも当面は互換性の面で実務投入は難しそうだ
    • メタプログラミングの利点は、もともと コードを書く量を減らすこと ではなかった
      読むべきコードを減らすこと だった
  • これは本当にすばらしく、私は長いこと Ruby向けAOT compiler を待っていた
    ただ eval やメタプログラミングのfallbackがないのは残念で、それでも小さく高性能なsubsetに集中するためにそうしたのだろう
    このAOTコンパイラで作ったgemがMRIとうまく相互運用できるといいのだが
    標準Rubyやgemをパッケージ化・バンドルする方向では、今でもtebako、kompo、ocranが必要で、以前にはruby-packer、traveling ruby、jruby warblerのようなプロジェクトもあった
    選択肢がひとつ増えるのは良いことだが、より良い開発者UXを備えた 決定版 が出てきてほしいと思っている

    • そう、その通りで、私も最近 warbler をforkしなければならなかった
      あまりにも長い間更新されていなかったからだ
  • なぜ no threads なのか気になる
    Ruby schedulerと下層のpthread実装はC側でもうまく動きそうだし、もしかしてzero dependencyを狙ったのだろうかと思う
    optional extensionとして後から入れる予定なのか、それとも単にまだ外してあるだけでないなら、この選択は少し奇妙に感じる

    • これを意図的に サポートしないと決めた という根拠はまだ見ていない
      たぶん、まだそこまで到達していないだけではないだろうか
      マルチスレッドはもともときちんと作るのが非常に難しい
  • 1か月少々で作ったというのは驚きだ
    AIについて何と言おうと、腕のある開発者 の手に渡ればとてつもない速度向上を生み出す

    • 業界全体ではagent harness、SOUL.md、権限設定、skills、MCPs、hooks、envを全部そろえて始めるのに、
      Matzはただ gem env|infofind だけで十分そうに見える
  • これがMatzの作ったものだとなると、今後 Ruby core の一部になる可能性がどれくらい現実的なのか気になる
    そして、そうなった場合Crystalにとってどれほど脅威になるのかも気になる

    • Crystalには明示的な static type system があり、言語レベルでAOTコンパイルに最適化されている
      こうした特性は大きなプログラムをコンパイルし、保守するうえで事実上不可欠に近い
      一方こちらは制限されたRuby subset向けなので、人気のあるRuby gemの多くはそのままでは動かないだろう
      Cコンパイルを志向する言語subsetという点では PreScheme により近く見える
      現時点では、両者が同じ領域で直接競合しているとは思わない
      完全なRubyにはほぼ確実にJITが必要だ
      [1]: https://prescheme.org/
    • 別の見方をすると、結局LLMが私たちの望むどんな言語に対しても formal specification を吐き出す地点に到達するのだと思う
      Rational Unified ProcessやEnterprise Architectの復讐が始まるようなものだ
      違いがあるとすれば、UMLダイアグラムの代わりにmarkdownファイルが来るくらいだ
  • これは infrastructure tools の分野で役立ちそうだ
    たとえばRubyで書かれていながら静的コンパイルされるbundlerがあって、RVMのようなRubyインストールツールの役割まで兼ねる、と想像できる
    既存のRuby buildpackはRubyで書かれているが、ブートストラップをbashでやらないといけないので面倒だし、edge caseも生まれる
    CNBはその問題を避けるためにRustで書かれており、依存関係のない単一バイナリ を配布できるという発想は本当に強力だ