Skip to content

Build the dep-graph reverse index lazily, per DepKind#157319

Draft
xmakro wants to merge 1 commit into
rust-lang:mainfrom
xmakro:perf/lazy-depkind-reverse-index
Draft

Build the dep-graph reverse index lazily, per DepKind#157319
xmakro wants to merge 1 commit into
rust-lang:mainfrom
xmakro:perf/lazy-depkind-reverse-index

Conversation

@xmakro
Copy link
Copy Markdown

@xmakro xmakro commented Jun 2, 2026

What this does

When a SerializedDepGraph is decoded at the start of an incremental session, it eagerly builds a fingerprint to index map for every DepKind, covering every node in the graph (one UnhashMap per kind, populated with a hash-map insert per node). That inverse index has a single consumer, node_to_index_opt, which is only reached for nodes the session queries directly (try_mark_green and a few DepGraph entry points). The overwhelming majority of the graph is reached by try_mark_previous_green walking edges, which reference nodes by index, not by fingerprint, so most of those per-kind maps are built and never read.

This makes the index lazy:

  • At decode time, a counting sort groups the node indices into one contiguous range per DepKind (using the per-kind counts already stored in the graph tail). This is a single pass of array writes, with no hashing.
  • The fingerprint map for a given DepKind is built the first time a node of that kind is looked up, via OnceLock. Kinds that are never looked up never build a map.

The on-disk format is unchanged; this only changes how the decoded graph is held in memory.

Why it is sound

node_to_index_opt returns the same index for the same DepNode as before. The map for a kind is built from exactly that kind's nodes, so lookups are identical. The only observable difference is that the duplicate-fingerprint assertion now runs lazily, per kind, the first time that kind is looked up, instead of for every kind up front. Kinds that are never looked up are not checked, which weakens a defensive "this should never happen" check for those kinds but does not change any result a lookup returns.

What was measured

Two stage1 librustc_driver shared objects were built from this tree, one at the parent commit (baseline) and one with the change. Only the .so is swapped between runs, so nothing else differs. The metric is callgrind instruction reads (Ir), which is deterministic, attributed to the crate's own rustc --crate-name <crate> invocation (the rustc child with the largest Ir).

This change only affects the incremental decode path, so the relevant scenario is an incremental rebuild that reloads a previous session's graph. The crate is built once to populate the on-disk incremental cache, then source mtimes are bumped with no content change and it is rebuilt under callgrind (CARGO_INCREMENTAL=1). A clean build (CARGO_INCREMENTAL=0) never decodes a previous graph, so it is unaffected by construction.

Incremental, unchanged

crate baseline Ir with change Ir delta Ir delta
regex 1.10 429,014,179 428,048,886 -965,293 -0.225%
ripgrep 14.1 1,027,196,966 1,025,095,611 -2,101,355 -0.205%
serde 1.0 3,065,152,702 3,060,643,354 -4,509,348 -0.147%
tokio 1.38 382,635,392 382,162,120 -473,272 -0.124%

The absolute saving is the per-node hash-map inserts no longer paid at decode for never-looked-up kinds, so it scales with the size of the serialized graph (largest on serde, smallest on tokio). It is a small fraction of a full check, so this is a modest win, but it is consistent and never regresses.

These are local measurements (deterministic callgrind Ir); marking the PR as draft so a perf run can confirm on the full suite.

Testing

  • ./x test tests/incremental passes (176 tests).
  • Incremental cargo check diagnostics are byte-identical to baseline on regex, ripgrep, syn and serde.

When a SerializedDepGraph is decoded, it built a fingerprint to index
map for every DepKind covering every node. That inverse index is only
consulted by node_to_index_opt, which runs for the nodes a session
queries directly; the bulk of the graph is reached as edge targets by
index and is never looked up by fingerprint, so most of those maps are
never read.

Replace the eager build with a counting sort that groups node indices
into a contiguous range per DepKind, and build the fingerprint map for a
kind only the first time a node of that kind is looked up. Decode no
longer pays a hash-map insert per node, and kinds that are never looked
up never build a map. The on-disk format is unchanged.
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jun 2, 2026
@rust-log-analyzer
Copy link
Copy Markdown
Collaborator

The job tidy failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
##[endgroup]
[TIMING:end] tool::ToolBuild { build_compiler: Compiler { stage: 0, host: x86_64-unknown-linux-gnu, forced_compiler: false }, target: x86_64-unknown-linux-gnu, tool: "tidy", path: "src/tools/tidy", mode: ToolBootstrap, source_type: InTree, extra_features: [], allow_features: "", cargo_args: [], artifact_kind: Binary } -- 11.807
[TIMING:end] tool::Tidy { compiler: Compiler { stage: 0, host: x86_64-unknown-linux-gnu, forced_compiler: false }, target: x86_64-unknown-linux-gnu } -- 0.000
fmt check
Diff in /checkout/compiler/rustc_middle/src/dep_graph/serialized.rs:191:
         let kind = self.index.kinds.get(dep_node.kind.as_usize())?;
         let map = kind.map.get_or_init(|| {
             let range = (kind.start as usize)..(kind.start as usize + kind.len as usize);
-            let mut map = UnhashMap::with_capacity_and_hasher(kind.len as usize, Default::default());
+            let mut map =
+                UnhashMap::with_capacity_and_hasher(kind.len as usize, Default::default());
             for &idx in &self.index.nodes_by_kind[range] {
                 let node = &self.nodes[idx];
                 if map.insert(node.key_fingerprint, idx).is_some()
fmt: checked 6879 files
Bootstrap failed while executing `test src/tools/tidy tidyselftest --extra-checks=py,cpp,js,spellcheck`
Build completed unsuccessfully in 0:00:46
  local time: Tue Jun  2 15:46:50 UTC 2026
  network time: Tue, 02 Jun 2026 15:46:50 GMT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants