mizchi

compiler-perf-profiling

vibe compiler の性能改善手順 — node --cpu-prof での実コンパイルのプロファイル取得、phase bench (__bench_ export) の実行、ホットスポットの読み方、修正→fixpoint→回帰の検証ループ。コンパイルが遅い・CI が遅い・bench の退行を調べるときに使う。

mizchi 30 1 Updated 1mo ago
GitHub

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 30

1. まず実コンパイルをプロファイルする (最重要)

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.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. 修正したら必ずこの順で検証する

# 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 を
    逐次読みするため、実行中プロセスの挙動が壊れる)。