| name | compiler-perf-profiling |
| description | vibe compiler の性能改善手順 — node --cpu-prof での実コンパイルのプロファイル取得、phase bench (__bench_ export) の実行、ホットスポットの読み方、修正→fixpoint→回帰の検証ループ。コンパイルが遅い・CI が遅い・bench の退行を調べるときに使う。 |
compiler perf profiling — 計測してから直す
vibe compiler (selfhost, linear/RC lane) の性能改善は「synthetic bench で当たり
をつける」より「実コンパイルを node --cpu-prof でプロファイルする」方が
圧倒的に速く核心に届く。#799 でこの手順により重量級コンパイルを 39s→4s
(約10倍) にした実績がある。
0. ワンコマンド: scripts/profile_compile.sh
§1 の cpu-prof + self-time 集計と §3 のメモリ統計 (heap 高水位 / linear
memory / RSS) を 1 コマンドに束ねたもの。まずこれを叩き、深掘りが必要に
なったら §1 以降の手作業に降りる。
scripts/profile_compile.sh /tmp/gen/stage2.wasm
scripts/profile_compile.sh /tmp/gen/stage2.wasm foo.vibe 30
1. まず実コンパイルをプロファイルする (最重要)
S2=<stage2.wasm>
VIBE_PREOPEN_DIR="$PWD" VIBE_FS_COMPILE=1 VIBE_SELFHOST_IMPORT_ABI=raw \
node --cpu-prof --cpu-prof-dir=/tmp --cpu-prof-name=compile.cpuprofile \
scripts/wasm_vibe_host_runner.js --invoke cli_main "$S2" \
lib/@vibe/compiler/tests/codegen_lexer_test.vibe /tmp/out.wasm __no_entry__
対象は「compiler closure を丸ごと compile する重いテスト」(codegen_test /
cli_test / selfhost_s5 など) が良い。self 時間の集計:
import json, collections
p = json.load(open("/tmp/compile.cpuprofile"))
nodes = {n["id"]: n for n in p["nodes"]}
c = collections.Counter()
for sid, dt in zip(p["samples"], p["timeDeltas"]):
c[nodes[sid]["callFrame"]["functionName"] or "(anon)"] += dt
total = sum(c.values())
for fn, us in c.most_common(20):
print(f"{us/1000:8.1f}ms {us*100/total:5.1f}% {fn[:100]}")
プロファイルの読み方
- wasm 関数は name section の名前で出る。user 関数は
foo_exp_lib__vibe_compiler_..._vibe 形式。生成 runtime helper は
__rt_str_eq / __rt_arr_get / __rt_rc_drop など (#799 で命名済み。
もし wasm-function[N] が上位に出たら name section の命名漏れ —
linked_compile.vibe の gen_sec_names に追記する)。
(garbage collector) = V8 側。wasm linear memory の grow コピーや
runner JS のアロケーション churn が原因のことが多い。
- runner JS の関数 (findClosureEnv 等) が上位に出たらハーネス側の異常を
疑う。#799 では
--invoke cli_main が WASI 慣習の _start で全コンパイルを
先に 1 回走らせ、二重コンパイルになっていた (現 runner は cli_main invoke
時に pre-start をスキップ。VIBE_FORCE_RUN_INIT=1 で旧挙動)。
- 過去の頻出パターン: 「巨大な flat 名前配列への文字列線形走査」
(array_contains_str / collect_free_vars 系)。ident ごと × 数千名 =
O(N²)。修正は「compile 単位で 1 回だけ Map index を作り CompileCtx に
載せる」(capture_name_index / collect_used_builtin_names を参照)。
2. phase bench (補助: 相対比較・退行検知向け)
bench "name" { ... } block は __bench_<name> として export される。
node runner の bench mode で回す:
VIBE_PREOPEN_DIR="$PWD" VIBE_FS_COMPILE=1 VIBE_SELFHOST_IMPORT_ABI=raw \
bash scripts/run_wasm_vibe_host_runner.sh --invoke cli_main "$S2" \
lib/@vibe/compiler/parser_bench.vibe /tmp/b.wasm __no_entry__
VIBE_PREOPEN_DIR="$PWD" bash scripts/run_wasm_vibe_host_runner.sh \
--invoke "__bench_selfhost/parse_deep_binop_chain" \
--bench-count 20 --bench-warmup 3 --bench-setup _start /tmp/b.wasm
phase bench: selfhost_{lexer,parser,checker,codegen}_bench.vibe。
bench の罠
- module-level let がある bench は
--bench-setup _start が必須
(module init 前に bench export を叩くと未初期化 global で即例外)。
_start は bench body を一巡 smoke 実行する。どれか 1 つの bench が
throw すると module ごと落ちる。
- ファイルを読む probe は
perform Fs::ReadFile ではなく direct-call
Fs::read_file で書く (unhandled perform は必ず throw する — #768/#794
の direct-call 規約)。
- bench 対象ファイルの stale 化に注意: パッケージ移動 (#753) で
syntax/lexer.vibe が 14 行の re-export shim になり、それを測り続けて
いた bench があった。~80µs など不自然に速い数字は corpus を疑う。
- 計測は無負荷で。バックグラウンドの回帰やビルドと同時に測らない。
3. vibe 固有の他の計測手段
| 手段 | 取れるもの |
|---|
Profiler::now_us | 任意区間の実時間 (cache_probe_*_bench_test 群が使用) |
Profiler::heap_bytes | 任意区間のアロケーション量 (bump-heap pointer。now_us の allocation 版 — bump は解放しないので delta = その区間の確保量) |
vibe test --coverage | 関数/分岐 hit bitmap (通ったか。回数は取れない) |
debug_trace (vibe.trace section) | 関数 entry の実行順列 (先頭 4096) |
runner --bench-count + --mem 系 | ns/op, bytes/op (bump-heap delta) |
メモリ計測 (runner 側、ゲスト変更不要)
VIBE_WASM_MEMORY_STATS=1: run/bench 終了時に [wasm-memory] ... pages= bytes= heap_ptr= rss= を stderr に 1 行出す。heap_ptr が bump 高水位。
参考値: 重量級 full-closure compile (codegen_lexer_test) 1 回で
heap ~362MB / RSS ~428MB (2026-07 計測)。
VIBE_PROFILE_MEMORY_MARKS=1: ゲストが Profiler::now_us を呼ぶ
たびに [profile-memory] mark=N ... を出す。profiled compile (full CLI
entry の --profile-tsv / VIBE_PROFILE_TSV) は stage 境界で now_us を
呼ぶので、stage ごとのメモリ推移が取れる。注: generation の stage2
(flat low-level entry) は profiled path を持たないため marks は出ない —
dist / vibe/cli entry の wasm で使う。
- ゲスト内で区間を自分で測るなら
Profiler::heap_bytes (上表)。
4. 修正したら必ずこの順で検証する
VIBE_SELFHOST_REGEN_MODULE_SOURCE=1 \
VIBE_SELFHOST_ADAPTER_MODULE_SOURCE_OUT=lib/@vibe/compiler/cli_adapter_module_source.vibe \
bash scripts/generate_bundle.sh
bash scripts/generations.sh build --out-dir /tmp/gen --stage3 --skip-run-validation
cmp /tmp/gen/stage2.wasm /tmp/gen/stage3.wasm
VIBE_SELFHOST_STAGE2_WASM=/tmp/gen/stage2.wasm bash scripts/unit_test_runner.sh
検証の罠 (実際に踏んだもの)
- アロケーション churn は RC lane で顕在化する: 毎呼び出しで大きな
入れ子構造 (CtFn/Array の表など) を再構築すると、bump では動くが
RC の in-process コンパイルテストが OOB で落ちる。module-level let で
メモ化する (module let は一度だけ初期化される —
fixtures/module_let_memo_test.vibe が仕様)。
- 並列回帰と cache 系テスト: persistent cache の状態を検査するテストは
並列 fan-out と競合する (runner は path に "cache" を含むテストを
シリアル tail で回す)。並列で 1 件だけ落ちたらまず単独再実行で切り分け。
- 実行中の回帰がある間に runner の .sh/.js を編集しない (bash は script を
逐次読みするため、実行中プロセスの挙動が壊れる)。