Green Means Same: How rustc Re-runs Every Query and Then Throws Most of the Work Away
rustc's incremental compiler re-executes queries and compares 128-bit fingerprints to decide whether the result is unchanged - the trick is in what it hashes and what it skips.
TL;DR: rustc does not cache “the previous answer” for most queries. It re-runs the query and hashes the result; if the 128-bit fingerprint matches the one recorded in the previous session, the node is green and downstream work is reused. That design forces fingerprints to be reversible (a node can be re-executed from a fingerprint alone), which is why
DefIds hash to a stableDefPathHashrather than to their session-local index.
There is a persistent myth about incremental compilation: that the compiler caches the outputs of the previous run and reuses them. rustc does not. The dependency graph stores fingerprints, not values. The compiler redoes the work and checks whether the answer came out the same.
The check
// compiler/rustc_middle/src/dep_graph/graph.rs:791-835
fn alloc_and_color_node(
&self,
key: DepNode,
edges: &[DepNodeIndex],
value_fingerprint: Option<Fingerprint>,
) -> DepNodeIndex {
if let Some(prev_index) = self.previous.node_to_index_opt(&key) {
// Determine the color and index of the new `DepNode`.
let is_green = if let Some(value_fingerprint) = value_fingerprint {
if value_fingerprint == self.previous.value_fingerprint_for_index(prev_index) {
// This is a green node: it existed in the previous compilation,
// its query was re-executed, and it has the same result as before.
true
} else {
// This is a red node: it existed in the previous compilation, its
// query was re-executed, but it has a different result from before.
false
}
} else {
// This is a red node, effectively: it existed in the previous compilation
// session, its query was re-executed, but it doesn't compute a result hash
// (i.e. it represents a `no_hash` query), so we have no way of determining
// whether or not the result was the same as before.
false
};Read the third branch. Some queries are declared no_hash — they have no
fingerprint, so there is nothing to compare, so the node is always red.
That is not a bug, it is a contract: if you declare no_hash, you are telling
the incremental system “my result may differ between runs and that is fine,
but I will never be green”.
The fingerprint has to be reversible
Here is the part that is easy to miss. When a node in the previous session
needs to be re-executed (because one of its dependencies went red), rustc
must reconstruct the query key — the DefId, the HirId, whatever — from
the information stored on disk. A random 128-bit hash would be useless for
that.
So rustc uses a structured fingerprint:
// compiler/rustc_middle/src/dep_graph/mod.rs:26-56
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum KeyFingerprintStyle {
/// The fingerprint is actually a DefPathHash.
DefPathHash,
/// The fingerprint is actually a HirId.
HirId,
/// Query key was `()` or equivalent, so fingerprint is just zero.
Unit,
/// The fingerprint is an opaque hash, and a key cannot be reconstructed from it.
Opaque,
}
impl KeyFingerprintStyle {
/// True if a key can _potentially_ be recovered from a key fingerprint
/// with this style.
#[inline]
pub const fn is_maybe_recoverable(self) -> bool {
match self {
KeyFingerprintStyle::DefPathHash
| KeyFingerprintStyle::Unit
| KeyFingerprintStyle::HirId => true,
KeyFingerprintStyle::Opaque => false,
}
}
}DefId keys use a DefPathHash, which is a stable hash of the item’s
definition path plus the crate’s stable id:
// compiler/rustc_span/src/def_id.rs:61-101
/// A `DefPathHash` is a fixed-size representation of a `DefPath` that is
/// stable across crate and compilation session boundaries. It consists of two
/// separate 64-bit hashes. The first uniquely identifies the crate this
/// `DefPathHash` originates from (see [StableCrateId]), and the second
/// uniquely identifies the corresponding `DefPath` within that crate.
///
/// There is a very small chance of hash collisions, which would mean that two
/// different `DefPath`s map to the same `DefPathHash`. Proceeding compilation
/// with such a hash collision would very probably lead to an ICE, and in the
/// worst case lead to a silent mis-compilation. The compiler therefore actively
/// and exhaustively checks for such hash collisions and aborts compilation if
/// it finds one.
So DefIds get a fingerprint that means something. HirIds get one that
also means something:
// compiler/rustc_middle/src/dep_graph/dep_node_key.rs:160-178
fn to_fingerprint(&self, tcx: TyCtxt<'tcx>) -> Fingerprint {
let HirId { owner, local_id } = *self;
let def_path_hash = tcx.def_path_hash(owner.to_def_id());
Fingerprint::new(
// `owner` is local, so is completely defined by the local hash
def_path_hash.local_hash(),
local_id.as_u32() as u64,
)
}and only genuinely opaque keys (a Span? a Ty?) fall back to a random hash
that cannot be reversed.
The collision strategy is asymmetric on purpose
There is a subtle design decision in how the two halves of a DefPathHash
are chosen:
// compiler/rustc_hir_id/src/definitions.rs:361-370
// We want *both* halves of a DefPathHash to depend on the crate-id of the defining crate.
// The crate-id can be more easily changed than the DefPath of an item, so, in the case of
// a crate-local DefPathHash collision, the user can simply "roll the dice again" for all
// DefPathHashes in the crate by changing the crate disambiguator (e.g. by bumping the
// crate's version number).
//
// Children paths will only hash the local portion, and still inherit the change to the
// root hash.
let def_path_hash =
DefPathHash::new(stable_crate_id, Hash64::new(stable_crate_id.as_u64()));The crate-id half is included in both halves. That way, if a collision happens inside a crate, bumping the crate’s version number re-rolls every hash in the crate at once. The alternative — hashing only the local part against the crate id — would mean an intra-crate collision could not be resolved without renaming items.
Re-verifying, but only 1/32 of the time
Storing fingerprints instead of values means a bug in the hashing code could silently produce a green node with a stale value. rustc does not trust its own hashing, but it also cannot afford to re-hash everything:
// compiler/rustc_query_impl/src/incremental.rs:82-103
/// Whether a value loaded from the on-disk cache should have its fingerprint
/// verified with `incremental_verify_ich`. If `-Zincremental-verify-ich` is
/// specified, re-hash results from the cache and make sure that they have the
/// expected fingerprint.
///
/// If not, we still verify a subset: re-hashing is too expensive to do for
/// every value. The subset rotates with the session count, covering the whole
/// cache every 32 sessions, and is deterministic so that a verification
/// failure reproduces on retry.
///
/// `to_smaller_hash` mixes both fingerprint halves because neither half is
/// evenly distributed on its own (`DefPathHash` keys share the
/// `StableCrateId`, `HirId` keys contain a sequential id).
pub(crate) fn should_verify_loaded_value(
tcx: TyCtxt<'_>,
dep_graph_data: &DepGraphData,
key_fingerprint: PackedFingerprint,
) -> bool {
let hash = Fingerprint::from(key_fingerprint).to_smaller_hash().as_u64();
hash % 32 == dep_graph_data.session_count() % 32
|| tcx.sess.opts.unstable_opts.incremental_verify_ich
}Read the comment about to_smaller_hash again, because it is the sort of
detail that only shows up after an incident:
// compiler/rustc_data_structures/src/fingerprint.rs:45-53
#[inline]
pub fn to_smaller_hash(&self) -> Hash64 {
// Even though both halves of the fingerprint are expected to be good
// quality hash values, let's still combine the two values because the
// Fingerprints in DefPathHash have the StableCrateId portion which is
// the same for all DefPathHashes from the same crate. Combining the
// two halves makes sure we get a good quality hash in such cases too.
Hash64::new(self.0.wrapping_mul(3).wrapping_add(self.1))
}DefPathHash fingerprints all share the same crate id in half 0, so half 0
alone is a terrible modulus source: hash % 32 would select the same 1/32 of
all nodes in the crate, forever, and the other 31/32 would never be
verified. Mixing the halves first fixes that.
The result is a scheme that verifies every cached value exactly once every 32 sessions, deterministically (so a failure reproduces), at a cost of about 3% of full verification.
Where it lives
compiler/rustc_middle/src/dep_graph/graph.rs—alloc_and_color_node.compiler/rustc_middle/src/dep_graph/mod.rs—KeyFingerprintStyle.compiler/rustc_middle/src/dep_graph/dep_node_key.rs— per-key fingerprint strategies.compiler/rustc_query_impl/src/incremental.rs— the probabilistic re-verification.compiler/rustc_span/src/def_id.rs—DefPathHash.
The takeaway: incremental compilation in rustc is a fingerprinting system,
not a caching system. Everything downstream — the reversible fingerprints,
the no_hash escape hatch, the probabilistic re-verification — follows from
that one decision.