Skip to content

Span-resilient incremental hashing and cached proc-macro expansion [-7.75% incr-patched, -1.21% incr-unchanged] - #88

Closed
xmakro wants to merge 4 commits into
mainfrom
perf/pmspan-span-resilience
Closed

Span-resilient incremental hashing and cached proc-macro expansion [-7.75% incr-patched, -1.21% incr-unchanged]#88
xmakro wants to merge 4 commits into
mainfrom
perf/pmspan-span-resilience

Conversation

@xmakro

@xmakro xmakro commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Incremental compilation is span fragile: inserting one line at the top of a file shifts every span below it, and the shifted positions leak into query keys, result fingerprints, and the on-disk cache. On a 300-struct serde crate, a one-line insertion at the top costs 41.3B instructions incrementally, against a 6.26B floor measured with -Zincremental-ignore-spans plus -Zcache-proc-macros. This stack closes most of that gap with no flags: the same edit drops to 10.9B (-74%), and to 9.05B with -Zcache-proc-macros. It also makes the derive expansion cache actually hit across span shifts; before this it got zero hits on any position-shifting edit and was a net regression there (42.0B with the flag vs 41.3B without).

One commit per mechanism, each independently useful:

1. Span-agnostic keying for derive_macro_expansion. The key fingerprint is now (parent module, macro DefPathHash, tokens hashed with span hashing disabled) instead of the raw (LocalExpnId, &TokenStream). Expansion indices shift whenever an edit changes how many expansions precede a derive, so keying on them guarantees misses. The query becomes no_hash: a span-only difference in the output tokens is deliberately treated as no change. The triple is collision-free for legal code because identical tokens include the item name and duplicate names in one module are rejected. Adds an incremental test that a span shift reuses the cached expansion.

2. Position-free ExpnData hashing, attr-arg span parenting, cached-token respan. ExpnHash hashed call-site positions, so one moved line reddened all macro-expanded HIR; it now hashes the call-site and def-site syntax context only, with uniqueness still provided by update_disambiguator (order-based, stable across shifts). Attribute argument tokens were hashed with unparented spans in all code, plain source included, churning every attributed item; they are now span-parented before lowering (only when incremental is on). Cached derive output tokens are respanned to a file-start anchor with root context, keeping each token's own context: cached spans previously round-tripped through line/col decode against the current file, so a stale line spilled into the next line and the hash drifted every session. The anchor choice matters: respanning to call site makes source_span of every generated def move with edits and re-runs typeck/MIR for all generated bodies; the file-start anchor keeps them position-stable.

3. Span-parent dep suppression for embed-only queries. track_span_parent makes every query that inspects a parented span record a dep on source_span(parent). For thir_body, typeck_root, mir_built, check_unsafety, check_match, and region_scope_tree, the results embed spans verbatim and rebase them on reuse, so the dep only buys wasted re-execution on position shifts. thir_body was the amplifier: it is no_hash, so any red dep makes it unconditionally red and drags the whole MIR chain along. A new TaskDepsRef::AllowIgnoringSpanParents variant skips recording span-parent deps for those kinds only. Sub-queries record their own deps under their own tasks, so suppression does not leak, and codegen tracks its own span reads, so debuginfo stays correct (verified: a 3-line shift moves DWARF line info by 3).

4. Offset-based span hashing and caching. The absolute span path now hashes (file stable id, offset in file, length) instead of line and column, and TAG_FULL_SPAN in the incremental cache encodes and decodes the same triple. Hash and codec must agree on what is position invariant or incremental_verify_ich reports unstable fingerprints; the codec's line/col round-trip was also the source of the drift bug above. This removes span_data_to_lines_and_cols from ICH entirely (CachingSourceMapView is gone from both the hashing context and the cache encoder, replaced by one-entry lookup_source_file caches). Byte anchoring invalidates the same edits line anchoring does, and is strictly better for same-length edits, where bytes after the edit do not move.

Perf, rustc-perf collector A/B, 6 crates x check/debug/opt, jemalloc dist config, geomean, flag off (what a normal build sees):

scenario change
full -0.16%
incr-full -0.44%
incr-unchanged -1.21%
incr-patched:println -7.75%

46 of 60 cells improved, none regressed at or above 0.25%. Largest: serde -22% to -24%, hyper -4.7% to -5.8%, ripgrep check -5.5%, cargo debug -3.7%.

Validation: tests/incremental clean, full tests/ui clean (21309 tests), forced -Zincremental-verify-ich across 9 successive span shifts in both flag states with no panics, same-length newline edit (the case where byte and line anchoring diverge) clean, DWARF line tables rebase with the edit, output of a cached-then-shifted serde_json build round-trips correctly, and mid-file derive insertion (which shifts later expansion disambiguators) correctly misses instead of reusing stale output.

Known tradeoff: with -Zcache-proc-macros, errors reported inside a cached expansion lose their exact position (help and expansion notes stay correct and line accurate via up-to-date ExpnData). Flag-off diagnostics are unchanged.

@xmakro

xmakro commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Split into independently defensible per-mechanism PRs for upstreaming: #90 (ExpnData position-free hashing) and #89 (attr-arg span parenting) are the flag-independent hashing pair, #91 (offset-based span hashing, isolated A/B), #92 (span-parent dep suppression), #93 (the -Zcache-proc-macros stack, based on #89). This PR stays as the integrated reference; cumulative numbers are in the description.

@xmakro xmakro closed this Aug 4, 2026
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