Skip to content

Carry green query values forward from the previous cache file - #110

Draft
xmakro wants to merge 1 commit into
perf/base-0809from
perf/cache-carry-0809
Draft

Carry green query values forward from the previous cache file#110
xmakro wants to merge 1 commit into
perf/base-0809from
perf/cache-carry-0809

Conversation

@xmakro

@xmakro xmakro commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Carry green query values forward from the previous cache file, rebased onto current main and measured.

Saving the incremental query cache used to require every value it contains to be in memory: values of green nodes that were never used during the session were loaded from the previous cache file by the promotion pass, only to be re-encoded, byte for byte equivalent, into the new file. On a warm rebuild this decode and re-encode round trip for the entire cache dominates the cost of saving. Self-profiling match-stress incr-unchanged shows the shape clearly: cache promotion 9% of self time, incremental value loading 10%, plus the re-encoding on top.

Instead, copy the previous cache file's data region into the new file verbatim and reference the values of green nodes at their old positions. Copying the region unchanged keeps every internal absolute reference valid (type and predicate shorthands, symbol offsets, allocation data); the lookup tables around the region are adjusted instead: source file indices preserve previous assignments, the allocation index offsets this session's entries, expansions merge by stable hash, and syntax context tables become generational, selected by the position a context id is decoded from. After eight carried generations the save falls back to a full re-encode, which compacts away dead data left behind by red nodes. Values are tagged with their node's key fingerprint instead of a session-local index so the load-side sanity check works for values of any age.

With this, values are only decoded when a query actually demands them and are never re-encoded. The promotion pass (exec_cache_promotions) and the per-query promotion machinery disappear entirely; the remaining bookkeeping is a color-map walk that costs a map insert per green node.

Local dist-fidelity A/B (stage2, ThinLTO plus jemalloc, clean from-scratch builds on both sides, instructions:u, full benchmark matrix, mean per cell group):

profile full incr-full incr-patched incr-unchanged
check +0.08% +0.11% -1.11% -13.30%
debug +0.06% +0.11% -2.68% -14.41%
opt +0.05% +0.09% -1.75% -14.28%

The biggest cells are the cache-heavy incremental-unchanged benchmarks: ctfe-stress -58%, ucd -45%, tuple-stress -42%, coercions -33%, match-stress -25%; incr-patched has coercions println at -24.7%. Every incr-unchanged cell across all three profiles improves (worst is -0.07%). Full and incr-full builds pay a small bookkeeping cost, worst cell +0.43%, and the worst regression in the whole matrix is +0.83% (an incr-patched cell).

Correctness: tests/incremental passes (180/180), and a five-session chained rebuild of syn (full, unchanged, unchanged, patched, unchanged) works, which exercises values being carried forward across multiple generations of cache files.

Interaction with verification: values that are carried forward are not loaded, so the sampled fingerprint verification only sees values that some query actually demanded. That matches what a real user build executes.

A companion experiment on the branch perf/durability-strata measured the other half of the durability idea, a linear wavefront promotion pass over the previous dep graph: promote every node whose dependencies are already green, in one index-order scan after lowering, instead of discovering green nodes through recursive marking. It is correct (same test suite passes) and covers 60 to 70% of the graph in one pass, but it measures as a small net cost, about +0.6 to +0.9% on incremental scenarios when added on top of this change: the recursive walk it replaces is already demand-driven and linear in the graph, so the scan only removes traversal shape, not work, and pays for an extra pass over the index space. It would become interesting only combined with promotion-encode removal, which is out of scope here, so it is not part of this PR.

Caveats:

  • Between compactions the carried region retains dead values of red or removed nodes; the eight-generation full re-encode bounds the growth. File size across long session chains is worth watching.
  • This was written against a July tree and rebased across a month of main drift; the conflict surface was small (the promotion machinery had landed in near-identical form in the meantime), but the on-disk-cache side deserves a careful re-review.

Saving the incremental query cache used to require every value it
contains to be in memory: values of green nodes that were never used
during the session were loaded from the previous cache file by the
promotion pass, only to be re-encoded, byte for byte equivalent, into
the new file. On a warm rebuild this decode and re-encode round trip
for the entire cache dominates the cost of saving.

Instead, copy the previous cache file's data region into the new file
verbatim and reference the values of green nodes at their old
positions. Copying the region unchanged keeps every internal absolute
reference valid: type and predicate shorthands, symbol offsets and
allocation data all stay at their offsets. The lookup tables around the
region are adjusted instead: source file indices preserve all previous
assignments and new files append after them, the allocation index
keeps the previous entries and offsets this session's, expansions are
keyed by their stable hash and merge, and syntax context tables become
generational, one per carried region, selected by the position a
context id is decoded from, since the raw ids are local to the session
that wrote them. After eight carried generations the save falls back
to a full re-encode, which compacts away dead data left behind by red
nodes and stale tables. Values are now tagged with their node's key
fingerprint instead of its session-local index, so the load side
sanity check keeps working for values of any age.

The promotion pass is deleted: green values no longer need to be in
memory at all, and its remaining purpose, referencing them in the next
session, is now a walk over the color map that costs a map insert per
green node. Values that are in memory anyway, because their query ran
this session, are still encoded freshly, and the load path prefers the
fresh entry.

On Windows the previous file cannot stay mapped while it is replaced,
so the carry is disabled there for now and green values that were not
loaded during the session are recomputed by the next one, matching
what already happened to never-loaded values before this change.

The header format version is bumped so caches written by earlier
compilers, whose footer layout differs, are discarded cleanly.
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.

1 participant