Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ランタイム最適化 — 目次と共通の前提

runtime_optimization/ は、Ruby のコアクラスが monoruby の中でどう表現され、 VM と JIT がその上のホットな操作をどう安くしているかを、クラスごとに 1 文書 ずつまとめたものである。各文書は末尾に CRuby との実装差異の表を持つ。 すべて日本語。

文書内容
array.mdSmallVec<[Value; 5]> の inline / ヒープ、[] / []= / << / リテラル / 多重代入の VM・JIT 経路、Ruby で書かれた each / map / initializesum / sort / hash の注記。共有配列を持たない理由
hash.mdinline(≤ 3 ペア)/ boxed 線形 / boxed 索引の 3 表現、vm 不要の prehashed probe、機械語の probe(Symbol / String キー)、2 つのハッシュ関数、Ruby で書かれた走査とそのプリミティブ、計測と残課題。ar_table / st_table との対比
string.mdSmallVec<[u8; 32]> と共有部分文字列(隠れ frozen root、copy-on-write)、エンコーディングと code range の O(1) 畳み込み、frozen リテラルと "lit".freeze、補間、<< / getbyte / setbyte / == の JIT、Hash の String キー
regexp.mdOnigmo とプロセス全体のパターンキャッシュ、FrozenLiteral としてのリテラル、LFP_SVAR$~ と 40 バイトの MatchDataMatchData を作らない経路、正規表現を使う String メソッドの高速経路、StringScanner

共通の前提

以下は 4 文書が暗黙に使う事実で、それぞれの出典を示す。

RValue は 64 バイト

ヒープオブジェクトはすべて RValuevalue/rvalue.rs)で、8 バイトの ヘッダ(live / frozen / chilled / GC の OLD・WB_UNPROTECTED・WB_ARMED / 年齢、型タグ ObjTy、型別メタデータバイト ty_flags、クラス)、8 バイトの var_table(あふれた ivar)、48 バイトのペイロード共用体 ObjKind からなる。Array / String / Hash はこの 48 バイトに「小さいものは直置き、 大きければヒープ」の 2 段を入れていて、閾値はそれぞれ 5 要素 / 32 バイト / 3 ペアである。値の表現(Fixnum / Flonum / 即値 / ヒープポインタ)は CLAUDE.md の「Value Representation」を参照。

fork した smallvec

Array と String のバッファは sisshiki1969/rust-smallvecconst_generics) で、fork している理由は OFFSET_CAPA / OFFSET_INLINE / OFFSET_HEAP_PTR / OFFSET_HEAP_LEN を公開して JIT / VM がペイロードを直接アドレッシング できるようにするためである。規約は「inline のあいだは capacity が長さを 兼ね、capacity > inline 容量 ならヒープ変種」で、機械語の高速経路はすべて cmp capa, N の 1 命令でこれを分岐する。

世代別 GC と write barrier

GC は非移動・stop-the-world・世代別(../gc.md)。子への参照を 持つオブジェクトが OLD へ昇格できるのは、その参照の格納がすべて write barrier を通る場合だけである。Array / Hash は mutator を barrier 付きの ラッパで隠し、String は隠れ root への 1 本の参照だけを barrier し、Regexp は Value を持たないので無条件に昇格できる。JIT がインラインで格納するときは emit_write_barrier_rdi / _bulk を自分で出す。

インライン生成器と BOP

JIT は組み込みメソッドの呼び出しを、InlineTable::add_inlineinline_gen2! で登録された生成器(抽象状態と AsmIR を受け取り、機械語を出す か false で辞退する)で置き換える。生成器は class-version ガードと レシーバクラスガードの後で呼ばれ、辞退すれば状態と IR が巻き戻される (trial inlining、../inline.md)。

Array#[] / Array#[]= / Hash#[] / String#freeze / String#!= など **基本演算(BOP)**として表(globals/store/basic_op.rs)に載っている組は、 class-version ガード無しの直接発火経路を取れ、再定義は記録した依存 (record_bop_dep)で該当本体だけを evict する (../bop_redefinition.md)。表に無いメソッドは 通常の class-version ガード付き経路に残る。

deopt と「call で逃がす」

型ガードの失敗はインタプリタへの side exit(deopt)になるが、deopt が 再コンパイルを起こさない場面で毎回 deopt するサイトは慢性的に遅くなる (../chain_deopt.md../deopt_log.md)。 そのため各文書に出てくる高速経路は、形が合わないケースを deopt でなく builtin への call で逃がす設計を繰り返し選んでいる: Hash の probe が inline / identity / 索引の表現を hashindex に渡す、Array#[] の多相 サイトが 2 腕の dispatch を出す、String#<< が fallback を末尾呼び出しする、 String#setbyte が共有レシーバを detach して再試行する、など。

Ruby で書かれた組み込み

builtins/*.rbArray#each / Hash#each / Array#initialize などが Ruby なのは、JIT がインライン展開するのが FuncKind::ISeq の呼び出し先だけ だからで、ホットなサイトではメソッド本体とブロックが yield の位置に展開 される。代償として、CRuby の C 実装なら無傷な Integer#<Array#size の 再定義が組み込みの内部に及ぶ(意図したトレードオフ、 ../bop_redefinition.md)。

計測の規律

各文書の数値は、同じ計測機で交互ラウンドの中央値の最小値を取り、 マイクロベンチは「同じループから対象操作だけ抜いたもの」を baseline として 差し引いている。monoruby と CRuby ではループ自体のコストが 10 倍近く違う ので、生の時間を並べても比較にならない。手順は hash.md §8。