Skip to content

perf: lazy §5 metadata windows on the demoted env load path - #585

Merged
johnchandlerburnham merged 2 commits into
mainfrom
jcb/decompile-load
Aug 21, 2026
Merged

perf: lazy §5 metadata windows on the demoted env load path#585
johnchandlerburnham merged 2 commits into
mainfrom
jcb/decompile-load

Conversation

@johnchandlerburnham

Copy link
Copy Markdown
Member

Headline numbers (Mathlib, 771,135 consts / 3.33 GB env, A/B on the same box)

ix decompile, section split from the new [Env::get] timers:

before after Δ
§5 named metadata parse 12.02 s 3.19 s −73 %
§2 consts (blake3 verify) 1.56 s 0.95 s −39 %
merkle root recompute 0.15 s 0.01 s −93 %
whole .ixe parse 16.56 s 7.17 s −57 %
decompile wall 107.1 s 97.1 s −9.4 %
peak RSS 31.6 GiB 30.3 GiB −1.3 GiB

Every pass timing is flat (muts plan 0.60→0.56 s, Pass 1 14.6→14.5 s, Pass 2 65.1→64.9 s): the work removed from the load was not pushed into the passes.

The same loader serves import_ixe, so a selective import (the TruthMines process-boundary use) stops paying a full §5 metadata parse for the whole env before materializing its first constant.

What changed

  • Lazy §5 metadata windows (the win): Env::get_demoted_named used to parse every §5 entry's metadata into the structured form and immediately re-serialize it into the demoted bytes repr — paying a full parse plus a full re-encode per entry, serially, before any pass starts. It now keeps each entry's meta_len-framed window verbatim (one contiguous arena plus the shared §4 reverse index) and decodes on demand: new MetaRepr::Window / LazyMetaWindow in env.rs, decoding through the same three grammar helpers get_named_indexed itself now parses with (single source for the window grammar). A OnceLock split memo (meta length + original address, ~40 B/entry) is filled by any meta decode — the decompiler's muts-plan sweep reads every entry's meta up front, so has_original()/original() on the passes stay as cheap as the old materialized slots. The mutating original-slot APIs materialize the window first, so Named stays total. RSS drops because the indexed windows are the compact encoding (varint name indices) while the old demoted blobs re-encoded every name reference as a raw 32-byte address.
    • Trade-off, documented on get_demoted_named: interior §5 corruption that the eager loaders reject at load now surfaces as a panic on first decode (window framing is still validated at load; §2 constants keep their per-entry hash check).
  • §2 integrity check parallelized: serial framing/order pass first, then the per-const blake3 verification sweeps in parallel (serial on the guest target, same cfg shape as the existing merkle pair).
  • Read-side merkle root: Env::get recomputed the root with the serial builder even though §2's order check proves the key set sorted and duplicate-free — now uses the parallel merkle_root_canonical_sorted from perf: decompile Pass-2 restructure + compile hot-path batch (post-#572 perf pass) #579 (host-only, same cfg pattern as the write side).
  • §4 name-table topo sort (write path): the recursive DFS is now an iterative ancestor-chain walk with a pre-sized visited set. The emission order — which is wire bytes — is reproduced exactly; proven by the whole-Mathlib byte compare.
  • [Env::get] per-section timers behind IX_VERBOSE/IX_COMPILE_DBG, mirroring put_file's — these produced the section split above.
  • §4 names-lookup pre-sizing (it rehash-doubled through 4.7M inserts; measured a wash at Mathlib scale, kept as principled).

Validation

  • Whole-Mathlib byte identity (write path): ix compile of CompileMathlib.lean at this branch vs the pre-branch reference → cmp identical (3,326,436,547 bytes).
  • Whole-Mathlib ix validate: 0 failures across all phases (compile, aux congruence, two decompiles through the lazy loader, serialize, per-constant roundtrip fidelity).
  • New test demoted_load_matches_structured_load: lazy load ≡ structured load on every Named accessor (meta, original, has_original, hints), mutation APIs materialize correctly, and the two loads re-serialize byte-identically — including entries whose metadata carries name references through the §4 index. (It also surfaced a pre-existing latent quirk, unchanged here: §4 emits the parent-closure of env.names, so a synthetic env whose multi-component names lack registered parents permutes §4 on reload. Pipeline envs register every component — whole-env byte roundtrips hold in CI.)
  • --ignored gates: validate-aux 0 failures; aux-gen-diff drift/patches/plans/driver gates PASS with all gated mismatch counts 0; decompile-diff plain 3,660/0 and aux-family 2,504/0, Pass-2 plan parity 300/300.
  • cargo test --workspace --release: all suites pass (0 failures).
  • Both CI clippy variants (--all-features dev and --release) with -D warnings, and cargo fmt --check: clean.

Explicitly not touched

Wire format (no Env::VERSION change), Env::get's eager/validating semantics, the Lean-side deserializers, and the decompile passes themselves. Remaining decompile targets after this: Pass 2 gen (59 s, the dominant phase — per-access LazyConstant re-parse territory) and Pass 1 (14.5 s), plus the prove-side items tracked separately.

The decompile pre-phase parsed every §5 named entry's metadata into the
structured form and immediately re-serialized it into the demoted bytes
repr — 12.0 s of the 16.6 s Mathlib `.ixe` parse, all serial, paid
before any pass starts. `Env::get_demoted_named` now keeps each entry's
meta_len-framed window verbatim (one contiguous arena + the shared §4
reverse index) and decodes on demand through the same grammar helpers
`get_named_indexed` parses with (`LazyMetaWindow`; a OnceLock split
memo makes `has_original` cheap after any meta decode — the muts-plan
sweep warms every entry before the passes). Both decompile FFI entries
use this loader, so `import_ixe` gets the same load-time cut. Wire
format untouched; `Env::get` still parses §5 eagerly and rejects
interior corruption at load (lazy loads surface it on first decode
instead — documented on `get_demoted_named`).

Also on the read path: §2 per-const blake3 verification became a
parallel sweep after the serial framing pass; the §4 names lookup is
pre-sized (it rehash-doubled through 4.7M inserts); the final
merkle-root check uses the parallel `_sorted` variant (§2 already
proved the key set sorted+unique). Write path: `topological_sort_names`
walks ancestor chains iteratively with a pre-sized visited set —
emission order (wire bytes) unchanged. New `[Env::get]` per-section
timers behind IX_VERBOSE/IX_COMPILE_DBG mirror `put_file`'s.

New test: demoted_load_matches_structured_load pins lazy ≡ structured
on every accessor plus byte-identical re-serialization (compared
between the two loads: §4 emits the parent-closure of `env.names`, so
synthetic envs without registered parent components permute §4 on
reload — pipeline envs register every component and are unaffected).
@johnchandlerburnham

Copy link
Copy Markdown
Member Author

!benchmark compile decompile

@argument-ci-bot

argument-ci-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown

!benchmark — main vs 4ba87b0

backends: compile decompile · envs: InitStd,Lean,Mathlib,FLT · set: primary · shard: 0

compile · FLT — main from: base run @ 58cc959 (not on bencher)

1 env · 0 with regressions · 1 with improvements (|Δ| > 3.0% on any metric).

env compile-time (main) compile-time (PR) Δ% throughput (const/s) (main) throughput (const/s) (PR) Δ% peak-ram (main) peak-ram (PR) Δ% env-size (main) env-size (PR) Δ% constants (main) constants (PR) Δ%
FLT 50.657 s 45.568 s -10.0% (1.11× faster) 🟢 15.42K 17.14K +11.2% (1.11× faster) 🟢 19.78 GiB 19.77 GiB -0.0% 3.20 GiB 3.20 GiB +0.0% 780,906 780,906 +0.0%

compile · InitStd — main from: base run @ 58cc959 (not on bencher)

1 env · 0 with regressions · 1 with improvements (|Δ| > 3.0% on any metric).

env compile-time (main) compile-time (PR) Δ% throughput (const/s) (main) throughput (const/s) (PR) Δ% peak-ram (main) peak-ram (PR) Δ% env-size (main) env-size (PR) Δ% constants (main) constants (PR) Δ%
InitStd 4.683 s 3.682 s -21.4% (1.27× faster) 🟢 25.00K 31.80K +27.2% (1.27× faster) 🟢 4.00 GiB 3.96 GiB -1.0% 324.65 MiB 324.65 MiB +0.0% 117,084 117,084 +0.0%

compile · Lean — main from: base run @ 58cc959 (not on bencher)

1 env · 0 with regressions · 0 with improvements (|Δ| > 3.0% on any metric).

env compile-time (main) compile-time (PR) Δ% throughput (const/s) (main) throughput (const/s) (PR) Δ% peak-ram (main) peak-ram (PR) Δ% env-size (main) env-size (PR) Δ% constants (main) constants (PR) Δ%
Lean 7.755 s 7.546 s -2.7% 26.65K 27.38K +2.8% 5.29 GiB 5.27 GiB -0.5% 481.18 MiB 481.18 MiB +0.0% 206,647 206,647 +0.0%

compile · Mathlib — main from: base run @ 58cc959 (not on bencher)

1 env · 0 with regressions · 1 with improvements (|Δ| > 3.0% on any metric).

env compile-time (main) compile-time (PR) Δ% throughput (const/s) (main) throughput (const/s) (PR) Δ% peak-ram (main) peak-ram (PR) Δ% env-size (main) env-size (PR) Δ% constants (main) constants (PR) Δ%
Mathlib 48.971 s 43.753 s -10.7% (1.12× faster) 🟢 15.75K 17.62K +11.9% (1.12× faster) 🟢 19.31 GiB 18.99 GiB -1.6% 3.10 GiB 3.10 GiB +0.0% 771,129 771,129 +0.0%

decompile · FLT — main from: base run @ 58cc959 (not on bencher)

1 constant · 0 with regressions · 1 with improvements (|Δ| > 3.0% on any metric).

constant decompile-time (main) decompile-time (PR) Δ% throughput (const/s) (main) throughput (const/s) (PR) Δ% peak-ram (main) peak-ram (PR) Δ% env-size (main) env-size (PR) Δ% constants (main) constants (PR) Δ%
FLT 1m 56.6s 1m 46.9s -8.4% (1.09× faster) 🟢 6.70K 7.31K +9.1% (1.09× faster) 🟢 33.54 GiB 32.16 GiB -4.1% 🟢 3.20 GiB 3.20 GiB +0.0% 780,906 780,906 +0.0%

decompile · InitStd — main from: base run @ 58cc959 (not on bencher)

1 constant · 0 with regressions · 1 with improvements (|Δ| > 3.0% on any metric).

constant decompile-time (main) decompile-time (PR) Δ% throughput (const/s) (main) throughput (const/s) (PR) Δ% peak-ram (main) peak-ram (PR) Δ% env-size (main) env-size (PR) Δ% constants (main) constants (PR) Δ%
InitStd 4.304 s 3.314 s -23.0% (1.30× faster) 🟢 27.20K 35.33K +29.9% (1.30× faster) 🟢 3.84 GiB 3.66 GiB -4.5% 🟢 324.65 MiB 324.65 MiB +0.0% 117,084 117,084 +0.0%

decompile · Lean — main from: base run @ 58cc959 (not on bencher)

1 constant · 0 with regressions · 1 with improvements (|Δ| > 3.0% on any metric).

constant decompile-time (main) decompile-time (PR) Δ% throughput (const/s) (main) throughput (const/s) (PR) Δ% peak-ram (main) peak-ram (PR) Δ% env-size (main) env-size (PR) Δ% constants (main) constants (PR) Δ%
Lean 9.125 s 7.705 s -15.6% (1.18× faster) 🟢 22.65K 26.82K +18.4% (1.18× faster) 🟢 4.97 GiB 4.80 GiB -3.3% 🟢 481.18 MiB 481.18 MiB +0.0% 206,647 206,647 +0.0%

decompile · Mathlib — main from: base run @ 58cc959 (not on bencher)

1 constant · 0 with regressions · 1 with improvements (|Δ| > 3.0% on any metric).

constant decompile-time (main) decompile-time (PR) Δ% throughput (const/s) (main) throughput (const/s) (PR) Δ% peak-ram (main) peak-ram (PR) Δ% env-size (main) env-size (PR) Δ% constants (main) constants (PR) Δ%
Mathlib 1m 46.7s 1m 36.9s -9.2% (1.10× faster) 🟢 7.23K 7.96K +10.1% (1.10× faster) 🟢 32.32 GiB 30.97 GiB -4.2% 🟢 3.10 GiB 3.10 GiB +0.0% 771,129 771,129 +0.0%

Workflow logs

@johnchandlerburnham
johnchandlerburnham added this pull request to the merge queue Aug 21, 2026
Merged via the queue into main with commit c8bfc1a Aug 21, 2026
13 checks passed
@johnchandlerburnham
johnchandlerburnham deleted the jcb/decompile-load branch August 21, 2026 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants