Skip to content

Compute the crate hash from encoded metadata in non-incremental builds [-1.00% non-incr full] - #61

Draft
xmakro wants to merge 3 commits into
perf/skip-span-hashing-nonincrfrom
svh-from-metadata
Draft

Compute the crate hash from encoded metadata in non-incremental builds [-1.00% non-incr full]#61
xmakro wants to merge 3 commits into
perf/skip-span-hashing-nonincrfrom
svh-from-metadata

Conversation

@xmakro

@xmakro xmakro commented Jul 27, 2026

Copy link
Copy Markdown
Owner

The crate hash is computed by stable-hashing the HIR. When nothing but the metadata needs it, hash the encoded metadata bytes instead: they are what downstream crates actually read, and they are already being written out.

-1.00% on non-incr full measurements (syn check -2.43%, serde check -2.29%, hyper check -1.98%, cargo check -1.87%), 7 cells >=1%, 0 regressions, max-rss -0.24%. tests/ui 21309 passed, tests/run-make 374 passed, tests/incremental 178 passed, 0 failed.

The metadata bytes replace only the hir_body_hash component, so the hash is assembled from three parts:

  • the encoded contents, hashed as they leave the encoder's write buffer. FileEncoder already supports a flush strategy, so this costs no extra pass over the file.
  • hash_crate_root, for the CrateRoot fields that appear nowhere else. Most of the root needs nothing: the Lazy* fields are positions into contents that are already hashed, and the rest is derived from tracked command line options. What is left are the crate level attribute flags such as compiler_builtins, needs_allocator and has_global_allocator, whose attributes are EncodeCrossCrate::No. The root is destructured exhaustively, so a new field has to be classified before it compiles.
  • the non-HIR components of tcx.crate_hash: tracked command line options, upstream crate hashes, remapped source file names and the crate's own stable id. Without these the hash stopped changing when an output type was added, which tests/run-make/emit-path-unhashed requires.

extra_filename is deliberately left out of all three. -Cextra-filename is untracked, and two rlibs built from the same source that differ only by it have to be recognized as the same crate (tests/run-make/extern-flag-fun).

The hash is only known once the file is complete, so the root is encoded with a zeroed placeholder that is patched afterwards, the same way the root position already is. METADATA_VERSION goes 10 -> 11 because hash becomes the first field of CrateHeader to give that placeholder a fixed offset; the patch reads it back and asserts it is still zeroed, so a later field reorder fails loudly rather than corrupting the metadata.

Proc-macro crates keep the HIR hash, since their metadata is a stub.

Worth knowing for review: needs_hir_hash() includes cfg!(debug_assertions), so none of this path runs on a compiler built with assertions.

@xmakro xmakro changed the title Compute the crate hash from encoded metadata in non-incremental builds Compute the crate hash from encoded metadata in non-incremental builds [-1.02% non-incr full] Aug 1, 2026
xmakro added 2 commits August 1, 2026 17:41
The crate hash was computed by flushing the encoder, seeking back to the
start of the file and reading every byte of it again through a scratch
buffer. Feed the bytes to the hasher as they leave the write buffer
instead, using the flush strategy `FileEncoder` already supports. That
drops the read-back and hashes the bytes while they are still in cache.

The hash is only known once the file is complete, so `encode_crate_root`
encodes a zeroed placeholder and `with_encode_metadata_header` patches
the real hash over it afterwards, the same way the root position is
already patched. To give the placeholder a fixed offset, `hash` becomes
the first field of `CrateHeader`, which is itself the first field of
`CrateRoot`, so it lands exactly at the root position. The patch reads
the placeholder back and asserts it is still zeroed, so a later field
reorder fails loudly rather than corrupting the metadata.

The hashed range stops before the root, which is where it stopped
before. The root holds `extra_filename`, which must stay out of the
crate hash: `-Cextra-filename` is untracked, and two rlibs built from
the same source that differ only by it have to be recognized as the same
crate.
The hash covered the encoded contents but not the `CrateRoot`, so a few
things it holds went unhashed. Cover them in the two ways that keep the
crate hash meaning what it meant before.

`hash_crate_root` hashes the root's own fields. It destructures the root
exhaustively, so a new field has to be classified before it compiles.
Most fields need nothing: the `Lazy*` ones are positions into contents
that are already hashed, and the rest are derived from tracked command
line options. What is left, and what this fixes, are the crate level
attribute flags such as `compiler_builtins`, `needs_allocator` and
`has_global_allocator`. Those attributes are `EncodeCrossCrate::No`, so
they appear nowhere else in the metadata. `extra_filename` is skipped;
it must not affect crate identity.

The non-HIR components of `tcx.crate_hash` are folded in as well. The
encoded bytes do not carry the tracked command line options, so without
this the hash no longer changed when, for instance, an output type was
added, which tests/run-make/emit-path-unhashed requires. It also means
only the `hir_body_hash` component is replaced by the metadata bytes;
everything else the crate hash covered, it still covers.

tests/ui 21309 passed, tests/run-make 374 passed, tests/incremental 178
passed.
@xmakro xmakro changed the title Compute the crate hash from encoded metadata in non-incremental builds [-1.02% non-incr full] Compute the crate hash from encoded metadata in non-incremental builds [-1.00% non-incr full] Aug 1, 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