Rust製のx86エミュレータ rustx86 で、ブラウザ (WASM) でも動く32bit Linuxを起動している。前回までにフルブート10秒 (ネイティブ66 MIPS) まで来ていて、目標は100 MIPS。「エミュレータの速さは以後すべての検証の単価」— 速くすればするほど、この先の全部の実験が短くなる。
今回はその続き。期待した最適化が効かず、ノーマークだった最適化が効き、そして4連敗して構造の上限に到達した一晩の記録である。数字は全部、交互A/B (新旧バイナリを交互に走らせて差だけを見る) での実測。
C1: lazy flags — 期待10〜25%、実測3%
x86のALU命令は毎回6個のフラグ (CF PF AF ZF SF OF) を書く。だがフラグは書かれる回数 >> 読まれる回数で、大半は次のALU命令に上書きされて誰にも読まれずに死ぬ。そこでQEMUと同じ cc_op 方式にした — 演算のたびにフラグを合成するのをやめ、材料 (op, a, b, carry, result) だけ控えて、読まれた瞬間に必要な1ビットだけ計算する。
// ALU本体 — フラグはここでは計算しない。材料を控えるだけ
fn alu_lazy(c: &mut Cpu, op: u8, a: u32, b: u32, w: u8) -> u32 {
let r = match op { /* 演算そのものだけ */ };
c.set_cc(op, w, a, b, cin, r); // ← 6フラグ分の合成が消える
if op == 7 { a } else { r } // CMPは書き戻さない
}
// 読む側: Jccが欲しいのは大抵1ビットだけ
pub fn flag(&self, mask: u32) -> bool {
if self.cc_op == CC_NONE || mask & CC_MASK == 0 {
return self.flags & mask != 0; // IF/DF等は今まで通り
}
match mask {
ZF => self.cc_r == 0, // ← cmp+je はこれ1個で済む
CF => self.cc_cf(),
// ...
}
}
実装で効いた設計判断が1つ。flags フィールドを private化して、読み書きを flag()/set_flag()/eflags()/set_eflags() の4つの口に集約した。シフトやMULのような「6フラグの一部だけ書く」少数派は set_flag() が先に具現化してから書くので、意味論は1ビットも変わらない。直アクセスしていた14箇所 (PUSHF・割り込み配送・スナップショット・Unicorn照合) はコンパイラが全部見つけてくれる — 規律を人間の注意力ではなく型システムに守らせる。
検証はいつもの関門: Unicornとのランダム照合 (cosim) 全緑、実カーネル1命令ずつ突き合わせ (kernel_lockstep) 完走、そして起動命令数がビット同一 (580M)。
で、結果は交互A/B 6周で -3〜4% (8.9→8.6s)。期待の10〜25%には遠く届かなかった。
なぜか。期待値はブロック連結 (B4) を入れる前の前提だったからだ。B4が毎命令の固定費を先に食い尽くしていて、残ったプロファイルの中でフラグ合成はもう小さな費目だった。前回のB3 (ペア融合) とまったく同じ構図 — 最適化の取り分は積み木ではない。先行する最適化が、後続の前提を書き換える。
C4b: プロファイルのmemmove 11%の正体
lazy flagsで空振りした後、プロファイルを取り直したら memmove がまだ11%残っていた。正体を追うと、#PF巻き戻し用のCPU丸ごとcloneだった。
このエミュレータは、命令の途中でページフォールトが起きたら「その命令は何も起きなかったことにして」巻き戻し、#PFを配送して再実行する。そのための控え (命令前のCPU状態の複写) を、メモリに触る命令の前に毎回取っている。この複写がCpu構造体まるごと約400バイト — XMMレジスタ128B、セグメント隠しレジスタ72B、デバッグレジスタ32B、全部入り。
だがキャッシュ済みuop (mov/ALU/push/pop/シフト…) の語彙に、セグメントレジスタやXMMを書く命令は存在しない。書き得るのは汎用レジスタ・IP・フラグだけ。なら控えもそれだけでいい:
/// キャッシュ済みuopが書き得る部分だけの薄い控え (~76B)
pub(crate) struct SlimSave {
regs: [u32; 8],
ip: u32,
flags: u32,
cc_op: u8, cc_w: u8, // lazy flagsの材料も控える
cc_a: u32, cc_b: u32, cc_cin: u32, cc_r: u32,
}
何でも起こせるフォールバック経路 (未実装命令・16bitコード) は従来どおり丸ごとcloneのまま。どちらの控えが有効かは enum が覚える。
これが -17% (8.9→7.4s)。期待していなかった側が、この夜最大の取り分だった。プロファイルの上位を「知っているつもり」で放置せず、正体を毎回追い直すのが正しい。
そして4連敗 — 構造の上限に触る
79 MIPSまで来て残り1.27倍。ここから台帳の候補を順に測った結果が壮観だった:
| 実験 | 仮説 | 実測 |
|---|---|---|
| TLBスロット数 (1K〜16K) | サイズ調整の余地 | 差なし (単発で速く見えたのは熱の運) |
| tickの一括払い | 毎命令の時計RMWが無駄 | +10%悪化 |
| ブロックのトレース化 | 1MBの表へのランダムloadがストール源 | +8%悪化 (6戦全敗) |
| 表の固定長配列化 | 境界チェック除去 | 差なし |
トレース化 (B5) は特に綺麗な反証になった。命令キャッシュを「バイト番地ごとの表」から「ブロック=連続uop配列」に組み替え、照合はブロック頭で1回・中身は連続読み、という教科書どおりの改善。命令数は不変、再デコードは15M→5Mに減り、構造は狙いどおりに動いて、それでも壁時計は遅くなった。
学びはこうまとめられる: M1のアウトオブオーダ実行・分岐予測・ストアバッファは、インタプリタの毎命令の照合も帳簿も既に隠蔽している。 その中をどう並べ替えても、1命令≒40サイクルの壁は動かない。コードが悪いのではなく、スイッチ型インタプリタという構造の上限である。
「M1が特殊なだけでは?」という疑いも実測で潰した。2012年のIvy Bridge実機 (後述) で同じA/Bを回したら、やはりトレース化は+4%悪化。ホストによらず割に合わない。 実験はタグ (exp/speed-b5-blocks) に保存して台帳に理由ごと記録した — 前提が変われば再訪できる。
おまけの収穫: 3ホスト×12構成で命令数がビット同一
Windows機とその上のUbuntu VMにもベンチ環境を作り (この構築記はコラムに分けた)、全組み合わせを測った:
| ホスト | native bzImage (970M) | native vmlinux (580M) | wasm bzImage (1000M) | wasm vmlinux (600M) |
|---|---|---|---|---|
| M1 Air (2020) | 10.7s / 90.7 MIPS | 7.3s / 79.5 | 16.2s / 61.7 | 10.0s / 60.3 |
| Ivy Bridge素 (2012) | 23.9s / 40.6 | 15.2s / 38.2 | 36.9s / 27.1 | 21.6s / 27.8 |
| 同PCのVirtualBox VM | 26.1s / 37.2 | 16.0s / 36.3 | 36.0s / 27.8 | 21.3s / 28.2 |
読みどころが3つある。
仮想化の税は5〜9%しかない (wasmではほぼゼロ)。エミュレータは純粋なユーザー空間演算で、VM exitをほぼ起こさないからだ。M1との2.4倍差の正体は、仮想化ではなくCPUの世代差 (2020 vs 2012) がほぼ全てだった — 「VMだから遅い」と言う前に、同一ハードのネイティブと並べて測るべきである。
wasm/ネイティブ比は67〜68%で、ISAによらず安定。 V8のwasm JITはarm64でもx86_64でも同じくらい仕事をする。
そして一番大事なのがこれ — 12マス全部で起動命令数がビット同一 (970M / 580M / 1000M / 600M)。このエミュレータは「決定性 (ビット同一再実行)」を売りにしていて、それがOS・ISA・ランタイム横断で成立していることが、ただのベンチのつもりだった表から証明された。
次: テンプレートJIT
インタプリタ内の再配置が全滅した以上、次は実行そのものを変える番 — デコード済みuop列をwasmコードに落とすテンプレートJITを本命に昇格した。設計方針はADR-0008に起こしてある。lazy flagsのcc_op方式は、実はこのJITのフラグモデルの土台でもある (生成コードはフラグを計算せず、ブロック出口で材料を書き戻すだけ)。空振りに見えたC1は、ここで回収される予定だ。
100 MIPSに届いたら店じまいして、次のマイルストーンへ — ANSI端末でDOOMを動かす。


コメント