
2026/03/19 6:26
ZJIT は冗長なオブジェクトのロードとストアを削除します。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
改善された概要
ZJIT は、インスタンス変数の処理を劇的に高速化する新しいロード・ストア最適化パスを導入しました。このパスは YJIT および Ruby インタープリタの両方を上回ります。setivar ベンチマークの時間は、YJIT の約 5 ms から ZJIT で約 2 ms に短縮され、YJIT の倍速、ベースラインインタープリタより 25 倍以上高速です。このパスは基本ブロックを走査し、
(object, offset) ペアをキャッシュして、冗長な LoadField / StoreField 命令を削除または置換します。キャッシュされたロードは取り除かれ、その使用箇所がキャッシュ値に置き換えられます。ストアは、同一の (object, offset, value) トリプレットが既に存在する場合にのみ削除されます。WriteBarrier などの変更命令でキャッシュエントリーはクリアされ、ストアが発生すると同じオフセットを持つすべてのエントリーが無効化されます(完全な型ベースのエイリアス解析はまだ実装中です)。このパスはブロック局所的であり、メソッド境界を越えません。また、依存関係のある StoreField は現在除外されています。既存のパス(type_specialize、inline など)の後、fold_constants の前に配置されます。将来のリリースでは型ベースのエイリアス解析を追加し、オブジェクトレベルで SSA を試験し、初期化コードへのデッドストア除去を拡張し、全体的にさらに多くの最適化パスを導入します。Ruby 開発者、とりわけインスタンス変数代入が頻繁なワークロードを実行している場合、この機能は実行速度を顕著に向上させ、CPU 消費を削減し、企業がアプリケーションのスケールアップをより効率的に行えるよう支援します。本文
イントロダクション
昨年末の投稿以降、ZJIT は大きく成長し、いくつか興味深い変化を遂げました。今回の記事では、新しく自己完結型の最適化パスが ZJIT の性能を YJIT を上回るマイクロベンチマークで示した経緯をご紹介します。ZJIT が Ruby に統合されてから 10 ヶ月が経ち、YJIT と ZJIT の設計差異が実際に性能の違いとして現れ始めています。
この記事では、ZJIT の HIR(High‑level Intermediate Representation)で導入された ロード/ストア最適化 について掘り下げます。ZJIT の構造は次のようになっています。
flowchart LR A(["Ruby"]) A --> B(["YARV"]) B --> C(["HIR"]) C --> D(["LIR"]) D --> E(["Assembly"])
Hir(高レベル中間表現)での最適化パスに焦点を当てます。HIR レイヤーでは、SSA(Static Single Assignment)表現と HIR の命令効果システムという他のコンパイル段階とは異なる二つの機能を活用できます。
現在実行される最適化パス(ロード/ストア最適化未導入時)
run_pass!(type_specialize); run_pass!(inline); run_pass!(optimize_getivar); run_pass!(optimize_c_calls); run_pass!(fold_constants); run_pass!(clean_cfg); run_pass!(remove_redundant_patch_points); run_pass!(eliminate_dead_code);
ロード/ストア最適化を追加後のパス
run_pass!(type_specialize); run_pass!(inline); run_pass!(optimize_getivar); run_pass!(optimize_c_calls); run_pass!(optimize_load_store); ← 新しいパス run_pass!(fold_constants); run_pass!(clean_cfg); run_pass!(remove_redundant_patch_points); run_pass!(eliminate_dead_code);
概要
Ruby はオブジェクト指向言語であるため、CRuby(MRI)は「オブジェクトのロード・変更・ストア」という概念を持つ必要があります。形状システムは CRuby(インタープリターと JIT の両方)に性能改善をもたらしますが、JIT の性能向上余地はまだ十分に残っています。オペコード単位で最適化を行うだけでは、繰り返し発生するロードやストアをプログラム解析パスで洗い出すことができません。この点について先に成果を振り返ってみましょう。
成果
setivar ベンチマークは 2026‑03‑06(ロード/ストア最適化が ZJIT に統合された日)に劇的に変化しました。執筆時点で、ZJIT はこのベンチマークで平均 2 ms/イテレーション、YJIT は 5 ms/イテレーション を記録しています。
下図は ZJIT(黄色)と YJIT(緑色)が「インタープリターより何倍速いか」(青色)を示したものです。ロード/ストア最適化が実装された瞬間に、ZJIT が YJIT を追い抜く様子が確認できます。
これは ZJIT が YJIT を明確に上回った二度目のケースです。初めての例はここにあります。
高レベルで言えば、ZJIT は繰り返しインスタンス変数への代入で YJIT の約 2 倍、さらにインタープリターより 25 倍以上 速く動作します。
心配な発見
オブジェクトのロードとストアに対する最適化パスが、実際にインスタンス変数代入にどう関係しているのでしょうか?答えは、ZJIT の HIR が
LoadField と StoreField を「オブジェクトのインスタンス変数と形状」両方で使用していることです。CRuby 形状と ZJIT HIR の内部構造を掘り下げてみましょう。
背景
HIR は
LoadField と StoreField を持ちます。これらは多目的に使われ、性能向上は主にオブジェクト形状の最適化から生じますが、インスタンス変数にも適用できます。アルゴリズムは両方で同様に機能するため、ここでは扱いやすさを重視してインスタンス変数に焦点を当てます。
例
以下のような簡単なコードには「二重ストア」があります。
class C def initialize value = 1 @a = value @a = value # 冗長なストア end end
この冗長な
StoreField 命令は HIR で除去されます。
Ruby ↔︎ HIR のマッピング
| Ruby | HIR |
|---|---|
| |
| |
クラスの
initialize 内ではインスタンス変数操作が形状遷移を引き起こすことが多く、initialize 外ではロードとストアは直接インスタンス変数にアクセスするケースが増えます。
エッジケース
以下のスニペットは
LoadField / StoreField を除去できる場合とできない場合を示します。
| ケース | コード |
|---|---|
| 冗長ストア | |
| 冗長ロード | |
| エイリアシング付きストア | |
| エイリアシングで必要なストア | |
| 副作用があるストア | |
アルゴリズム
核心アイデア: オブジェクトに対して軽量な抽象解釈を行う。ロードの置換とストアの除去はプログラム挙動を変えない範囲で実施されますが、機会損失もあるかもしれません。
- 基本ブロック: 各基本ブロックを走査し、冗長なロード/ストアを検索・更新します。
は直接削除可能ですが、StoreField
の参照はキャッシュされた値に置き換えます。LoadField - WriteBarrier: ガーベジコレクション用の書き込みバリアは「ストア」と同様に扱い、関連するキャッシュエントリをクリアしますが削除は行いません。
- ポインタの詳細: HIR 命令は常にオブジェクトベースと境界内のオフセットで参照します。異なるオフセットは同一メモリ領域を指さないため、エイリアシングチェックが必要です。
擬似コード
for each basic block: cache = {} # key: (object, offset) → value for each instruction: if LoadField: key = (obj, offset) if key in cache: replace all uses with cached value delete this instruction else: cache[key] = loaded_value elif StoreField: key = (obj, offset) if key in cache and cache[key] == stored_value: delete instruction # 冗長ストア else: remove all cache entries with same offset # エイリアシング回避 elif WriteBarrier: remove all cache entries with same offset elif instruction can modify objects: flush entire cache return pruned HIR
ソースコード
実装は ZJIT リポジトリに公開されています(リンクは省略)。
HIR の改善例
最適化後、冗長ロード/ストアが消えます。以下はその一例です。
冗長ロード除去
bb3(v8:BasicObject, v9:NilClass): ... StoreField v30, :@a@0x10, v13 WriteBarrier v30, v13 ... - v40:BasicObject = LoadField v20, :@a@0x10 - Return v40 + Return v13
冗長ストア除去
bb3(v8:BasicObject, v9:NilClass): ... StoreField v35, :@a@0x10, v13 WriteBarrier v35, v13 ... - StoreField v20, :@a@0x10, v13 + # 削除済み Return v13
デザインの議論
このパスはオブジェクトに対するロード/ストアのグラフを剪定します。SSA と同様の問題(オブジェクトレベルでの SSA)を解決しつつ、完全な SSA を導入すると HIR の構造が大幅に変わり、他の部分が扱いづらくなる恐れがあります。軽量な SSA 表現を維持することで、Hir 全体をシンプルに保ちつつ、確実な最適化を可能にしています。
今後の展開
- デッドストア除去: さらに初期化性能を向上させるためのアイデア。
- タイプベースのエイリアス解析: より積極的な除去を実現するが、型混同バグを防ぐ慎重な設計が必要(phrack 記事 4.1 章参照)。
結論
ZJIT の最適化パートに関する初めての記事をご覧いただきありがとうございます。今後も継続的にアップデートしていく予定ですので、ぜひご期待ください!