- YJITとZJITは、Ruby 3.xでRubyコードを機械語に変換して実行速度を高めるJITコンパイラの仕組み
- YJITは各関数やブロックの呼び出し回数をカウントし、一定のしきい値に達するとそのコードを機械語に変換
- 変換されたコードはYJITブロックに保存され、各ブロックは複数のYARV命令に対応するARM64機械語命令へ変換される
- Branch Stubを使ってランタイム時に実際のデータ型を観察し、それに応じた機械語命令を選択的に生成
- この仕組みは、Rubyの実行性能向上と動的型処理の効率性を同時に実現するための中核メカニズム
Chapter 4: Rubyを機械語にコンパイルする
Interpreting vs. Compiling Ruby Code
Counting Method and Block Calls
- YJITはプログラムの関数およびブロックの呼び出し回数を追跡してホットスポットコードを識別
- 各関数やブロックのYARV命令シーケンスの横にjit_entryとjit_entry_callsの値を保存
jit_entryは初期状態ではnullで、後からYJITが生成した機械語コードへのポインタを保存
jit_entry_callsは呼び出されるたびに1ずつ増加
- 呼び出し回数がしきい値に達すると、YJITがそのコードを機械語へコンパイル
- Ruby 3.5のデフォルトしきい値は小規模プログラムで30回、大規模アプリケーションで120回
- 実行時に
--yjit-call-thresholdオプションで変更可能
- この方式により、YJITは頻繁に実行されるコードだけを機械語に変換し、効率的な実行経路を確保
YJIT Blocks
- YJITは生成した機械語命令をYJITブロックに保存
- YJITブロックはRubyのブロックとは異なり、YARV命令の一部分の区間に対応
- 各Ruby関数やブロックは複数のYJITブロックで構成される
- 例のプログラムでは、ブロックが30回目に実行されたときにYJITがコンパイルを開始
- 最初のYARV命令
getlocal_WC_1を機械語へ変換して新しいYJITブロックを生成
- その後
getlocal_WC_0命令を追加でコンパイルし、同じブロックに含める
- Figure 4-8によると、YJITはARM64命令を生成し、M1プロセッサのx1、x9レジスタに値をロード
getlocal_WC_1は1つ前のスタックフレームのローカル変数を、getlocal_WC_0は現在のスタックの変数をスタックへ保存
- 生成された機械語命令は同じ動作を実行する
YJIT Branch Stubs
- YJITが
opt_plus命令をコンパイルするとき、オペランドの型が分からない問題が発生
- 整数、文字列、浮動小数点数など、型によって必要な機械語命令が異なる
- 例: 整数加算では
adds命令を使い、浮動小数点加算では別の命令が必要
- これを解決するため、YJITは事前分析ではなくランタイム観察方式を採用
- プログラム実行中に実際に渡された値の型を確認し、それに合った機械語を生成
- この動作のためにBranch Stubを使用
- 新しい分岐(branch)にまだ接続されたブロックがない場合、一時的にstubへ接続
- その後、実際の型が確認されると、そのstubを適切なブロックに置き換える
ZJIT(言及のみ)
- 目次にはZJIT関連のセクションが含まれているが、本文には具体的な説明はない
要約
- YJITはRuby 3.5における動的型付け言語の実行効率を高めるためのJITコンパイラ
- 呼び出し回数ベースのコンパイルトリガー、YJITブロック構造、Branch Stubによるランタイム型確認が中核
- ARM64アーキテクチャ上で実際の機械語命令へ変換し、Rubyコードの実行速度を向上
- ZJITは次世代JITとして言及されているが、詳細は本文にない
1件のコメント
Hacker Newsの意見
以前、MacRuby が LLVM を使って macOS 上でネイティブコードにコンパイルされ、Objective‑C フレームワークと統合されていた時代があった
かなり素晴らしいアイデアだったが、結局 Apple は Swift に舵を切ったようだ
新しい版が出たら Ruby Under a Microscope をぜひ買って読もうと思っている。Ruby は今でも好きだが、実際に使う機会はあまりなかった
今は別の人たちが引き継いでいるが、現在は DragonRuby(ゲーム中心の Ruby 実装)により力を入れている雰囲気だ
ちなみに ウィキ文書 もある
ただし、昔の API はもうサポートされていない可能性がある
VB6 は開発速度が本当に速く、Direct3D や ASP Classic まで扱えた
Ruby の 優雅さと開発のしやすさ があの時代を思い出させる
もし Ruby に VB6 級の GUI ツールがあったなら、人気もかなり違っていた気がする
Pat が引き続きプロジェクトを続けているのを見るのは本当にうれしい
彼の最初の Ruby Under a Microscope の本とブログ記事は、私に大きな 刺激 を与えてくれた
以前 Euruko カンファレンスで直接会ったこともあるが、本当に素晴らしい人だった
初めて Ruby Under a Microscope を読んだときは本当に面白かった
そのおかげで、以前 CTF の問題解き にも活用した
最近は Ruby の内部実装を追えていないが、新しい版が出たら必ず買うつもりだ
今回の記事を見て、新版をまた読みたくなった
Ruby コンパイルの話が出たので聞きたいのだが、Stripe の開発者たちが作った Sorbet compiler を使ってみたことはあるだろうか
Sorbet Compiler 公開記事
AOT コンパイル は Ruby では本当に難しい
Sorbet のアプローチが興味深いのは、Ruby の 型検査 を基に高速な経路を作れるからだ
私も個人プロジェクトとして Ruby コンパイラを作っていて、hokstad.com/compiler と
writing-a-compiler-in-ruby を参考にしている
今は RubySpec の通過に集中していて、後では型ベースの最適化にも挑戦してみるつもりだ
Ruby コンパイルとは直接関係ないが、Enterprise Integration with Ruby という本は、Web 以外の領域で Ruby を活用するうえで大きな 洞察 を与えてくれた
MRuby を知って以来、自分のプロジェクトやスクリプトをスタンドアロンの実行ファイルに変える楽しさにハマっている
Ruby Under a Microscope が今でも更新されているのはうれしい
Ruby の内部動作を理解したい人にとっては 必読書 だと思う
YJIT のブロックが何度も実行されるとき、入力型ごとにどのようにコンパイルを追跡しているのか気になっていた
Ruby が int や float などのさまざまな型をどう処理するのか知りたい
実際の型が与えられるまでコンパイルを遅らせる “wait‑and‑see” アプローチを使っている
型ごとにブロックのバージョンを別々に管理し、状況に応じて呼び分ける
このアルゴリズムは Basic Block Versioning と呼ばれる
Shopify の Maxime Chevalier‑Boisvert が RubyConf 2021 の発表動画 でうまく説明している
新しい JIT エンジンである ZJIT は別の方式を使っているようだ
動的型付け言語を JIT で高速化するには、通常 メモリ使用量の増加 という代償を払うことになる
Shopify のような大企業でなければ、こちらのほうが大きな問題かもしれない
最近のクラウドインスタンスはコアあたり 4GiB 程度のメモリを持っているので、数百 MB の JIT コード程度なら十分に耐えられる
YJIT が関数呼び出し回数だけを数えて ホットスポット を見つける方式は単純に見えた
JavaScript JIT のように、ループ内部の重い演算を検知する機能はないのだろうかと気になった
Ruby のブロック構造がこうした最適化に役立つかもしれないと思った
JIT はブロックを別個の関数のように扱いながら、反復処理を自然に 最適化 できる
この点は次の章でさらに深く扱う予定だ