vibe compiler の性能改善手順 — node --cpu-prof での実コンパイルのプロファイル取得、phase bench (__bench_ export) の実行、ホットスポットの読み方、修正→fixpoint→回帰の検証ループ。コンパイルが遅い・CI が遅い・bench の退行を調べるときに使う。
Install
npx skillscat add mizchi/vibe-lang/compiler-perf-profiling Install via the SkillsCat registry.
SKILL.md
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 # 既定 corpus
scripts/profile_compile.sh /tmp/gen/stage2.wasm foo.vibe 301. まず実コンパイルをプロファイルする (最重要)
S2=<stage2.wasm> # 現行 stage2 (scripts/generations.sh build の成果物)
VIBE_PREOPEN_DIR="$PWD" VIBE_FS_COMPILE=1 VIBE_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 に追記する)。 - ADR-0077 以降、release ビルドは name section を既定で strip する。
プロファイル対象の stage2/CLI wasm はVIBE_WASM_NAMES=1を付けて
ビルドし直すこと (例:VIBE_WASM_NAMES=1 bash scripts/generations.sh build --out-dir /tmp/prof_gen)。全フレームがwasm-function[N]に
なったら strip 済み wasm を profiling している。 (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 で回す:
# compile (テストと同じ __no_entry__ sentinel で OK)
VIBE_PREOPEN_DIR="$PWD" VIBE_FS_COMPILE=1 VIBE_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__
# 実行 (合計 µs が出る。/N して µs/iter)
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.wasmphase 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-callFs::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/clientry の wasm で使う。- ゲスト内で区間を自分で測るなら
Profiler::heap_bytes(上表)。
4. 修正したら必ずこの順で検証する
# a. bundle regen (compiler source を触ったら必須)
VIBE_REGEN_MODULE_SOURCE=1 \
VIBE_ADAPTER_MODULE_SOURCE_OUT=lib/@vibe/compiler/_cli_adapter_module_source.vibe \
bash scripts/generate_bundle.sh
# b. stage rebuild + fixpoint (stage2 == stage3 が絶対条件)
bash scripts/generations.sh build --out-dir /tmp/gen --stage3 --skip-run-validation
cmp /tmp/gen/stage2.wasm /tmp/gen/stage3.wasm
# c. 最適化の同値性: 修正前 stage2 と修正後 stage2 で同じファイルを
# コンパイルし、出力 wasm を byte 比較 (純最適化なら一致するはず)
# d. 再プロファイル (狙ったホットスポットが消えたか)
# e. 全体回帰
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 を
逐次読みするため、実行中プロセスの挙動が壊れる)。