From 52996af5c20fcde00bd6a7f1c5993c5a8ccf02ce Mon Sep 17 00:00:00 2001 From: Steve Klabnik Date: Wed, 12 Aug 2026 10:41:19 -0500 Subject: [PATCH] RUE-1465: defer object digests until plan comparison --- crates/rue-compiler/src/object_query.rs | 45 ++++++++++++---- crates/rue-compiler/src/program_image_plan.rs | 54 +++++++++++++++---- ...r-0063-cold-compiler-architecture-audit.md | 16 ++++++ 3 files changed, 96 insertions(+), 19 deletions(-) diff --git a/crates/rue-compiler/src/object_query.rs b/crates/rue-compiler/src/object_query.rs index e0526380b..895e977eb 100644 --- a/crates/rue-compiler/src/object_query.rs +++ b/crates/rue-compiler/src/object_query.rs @@ -5,7 +5,10 @@ //! consumes these stable bytes on every link, while unchanged units reuse the //! retained projection owned by the compiler query graph. -use std::{hash::Hash, sync::Arc}; +use std::{ + hash::Hash, + sync::{Arc, OnceLock}, +}; use rue_query::{ QueryAbort, QueryContext, QueryFamily, QueryKey, QueryOutcome, QueryOutput, QueryTerminalKind, @@ -74,25 +77,43 @@ impl QueryKey for ObjectProjectionQueryKey { } /// Retained object-container bytes and their stable content identity for one -/// codegen unit. The digest is computed once with the immutable bytes so every -/// downstream program-image assembly can reuse it without a serialized hash -/// pass. -#[derive(Debug, Clone, PartialEq, Eq)] +/// codegen unit. Fresh linking consumes only the immutable bytes, so the +/// durable digest is computed once on demand if a later plan comparison needs +/// it. +#[derive(Debug, Clone)] pub(crate) struct ObjectProjection { pub(crate) bytes: Arc<[u8]>, - pub(crate) content_digest: ContentDigest, + content_digest: OnceLock, } impl ObjectProjection { pub(crate) fn from_bytes(bytes: Vec) -> Self { - let content_digest = bytes_digest(OBJECT_DIGEST_DOMAIN, &bytes); Self { bytes: bytes.into(), - content_digest, + content_digest: OnceLock::new(), } } + + pub(crate) fn content_digest(&self) -> ContentDigest { + *self + .content_digest + .get_or_init(|| bytes_digest(OBJECT_DIGEST_DOMAIN, &self.bytes)) + } + + #[cfg(test)] + pub(crate) fn content_digest_is_initialized(&self) -> bool { + self.content_digest.get().is_some() + } } +impl PartialEq for ObjectProjection { + fn eq(&self, other: &Self) -> bool { + self.bytes == other.bytes + } +} + +impl Eq for ObjectProjection {} + impl RetainedCharge for ObjectProjection { fn retained_charge(&self) -> u64 { self.bytes.retained_charge() @@ -207,8 +228,12 @@ mod tests { let same = ObjectProjection::from_bytes(vec![1_u8, 2, 3, 4]); let changed = ObjectProjection::from_bytes(vec![1_u8, 2, 3, 5]); - assert_eq!(first.content_digest, same.content_digest); - assert_ne!(first.content_digest, changed.content_digest); + assert!(!first.content_digest_is_initialized()); + assert_eq!(first, same); + assert!(!first.content_digest_is_initialized()); + assert_eq!(first.content_digest(), same.content_digest()); + assert_ne!(first.content_digest(), changed.content_digest()); + assert!(first.content_digest_is_initialized()); } #[test] diff --git a/crates/rue-compiler/src/program_image_plan.rs b/crates/rue-compiler/src/program_image_plan.rs index 1a20e5e5a..a8da775f8 100644 --- a/crates/rue-compiler/src/program_image_plan.rs +++ b/crates/rue-compiler/src/program_image_plan.rs @@ -25,16 +25,30 @@ use crate::{ use crate::codegen_query::CollectedCodegenUnit; /// Stable, link-relevant identity for one reached codegen terminal. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] pub(crate) struct ProgramImageUnit { /// The durable callable identity retained by the terminal. The encoded /// key below is only its deterministic ordering/map projection. pub(crate) function: crate::FunctionInstanceKey, pub(crate) identity: String, pub(crate) defined_symbol: Arc, - pub(crate) content_digest: ContentDigest, + /// The immutable projection owns its lazily materialized durable digest. + /// Keeping the projection here lets fresh plan construction avoid hashing + /// bytes that only a later plan comparison needs. + pub(crate) object: Arc, +} + +impl PartialEq for ProgramImageUnit { + fn eq(&self, other: &Self) -> bool { + self.function == other.function + && self.identity == other.identity + && self.defined_symbol == other.defined_symbol + && self.object.content_digest() == other.object.content_digest() + } } +impl Eq for ProgramImageUnit {} + /// The deterministic contribution of a C-ABI export thunk. Thunks are not /// codegen terminals, but their object bytes are a compiler-owned link input /// and therefore have a separate, explicit plan entry. @@ -319,7 +333,7 @@ impl ProgramImagePlan { function: collected.function.clone(), identity, defined_symbol: collected.unit.defined_symbol.clone(), - content_digest: collected.object.content_digest, + object: Arc::clone(&collected.object), }) .collect::>(); plan_units.sort_by(|left, right| left.identity.cmp(&right.identity)); @@ -360,7 +374,7 @@ impl ProgramImagePlan { function: collected.function.clone(), identity: stable_function_identity(&collected.function), defined_symbol: collected.unit.defined_symbol.clone(), - content_digest: collected.object.content_digest, + object: Arc::clone(&collected.object), }) .collect::>(); plan_units.sort_by(|left, right| left.identity.cmp(&right.identity)); @@ -726,12 +740,32 @@ mod tests { .unwrap() } + #[test] + fn fresh_plan_defers_object_digests_until_comparison() { + let (plan, _, _) = image_for("fn main() -> i32 { 0 }", Target::X86_64Linux, 1); + assert!( + plan.units + .iter() + .all(|unit| !unit.object.content_digest_is_initialized()) + ); + + assert_eq!(plan, plan.clone()); + assert!( + plan.units + .iter() + .all(|unit| unit.object.content_digest_is_initialized()) + ); + } + fn unit(identity: &str, digest_byte: u8) -> ProgramImageUnit { ProgramImageUnit { function: crate::FunctionInstanceKey::DropGlue(Box::new(crate::TypeInstanceKey::I64)), identity: identity.to_owned(), defined_symbol: Arc::from(identity), - content_digest: [digest_byte; 32], + object: Arc::new(crate::object_query::ObjectProjection::from_bytes(vec![ + digest_byte; + 32 + ])), } } @@ -769,11 +803,13 @@ mod tests { #[test] fn delta_tracks_serialized_object_bytes_not_only_codegen_metadata() { let mut before = unit("same", 1); - before.content_digest = - bytes_digest(b"rue.program-image.object\0v1\0", b"first object encoding"); + before.object = Arc::new(crate::object_query::ObjectProjection::from_bytes( + b"first object encoding".to_vec(), + )); let mut after = before.clone(); - after.content_digest = - bytes_digest(b"rue.program-image.object\0v1\0", b"second object encoding"); + after.object = Arc::new(crate::object_query::ObjectProjection::from_bytes( + b"second object encoding".to_vec(), + )); let delta = plan(vec![after.clone()]) .delta_from(&plan(vec![before])) .unwrap(); diff --git a/docs/notes/post-adr-0063-cold-compiler-architecture-audit.md b/docs/notes/post-adr-0063-cold-compiler-architecture-audit.md index 7e6d8de69..7f5452b99 100644 --- a/docs/notes/post-adr-0063-cold-compiler-architecture-audit.md +++ b/docs/notes/post-adr-0063-cold-compiler-architecture-audit.md @@ -1786,6 +1786,22 @@ MAD); clock is neutral at -0.1323% amid substantial host noise, cycles at compiler-work counter, source/output metric, and executable byte remains identical. +RUE-1465 defers each immutable object projection's durable content digest until +a caller compares program-image plans. Fresh linking consumes the retained +object bytes directly, so eagerly hashing 1,280 objects and 3,011,805 bytes on +cold Lattice prepared identity for only the dormant later-linking seam. The +projection still owns and memoizes the same domain-separated SHA-256 digest; +ordinary exact object equality and fresh plan construction do not force it, +while plan delta comparison does. + +Across 16 balanced fixed one-worker cold Lattice pairs, retired instructions +improve by 0.5993% (0.0651% MAD) and cycles by 0.2315% (0.7333% MAD). +End-to-end clock is neutral at -0.1895% (1.4135% MAD), peak RSS at +0.3510% +(0.3469% MAD), and peak footprint at +0.8275% (0.3708% MAD). Four +allocation-accounted pairs are neutral at +101--320 calls (under 0.003%) and +-3.3--118.6 KB requested bytes. Every compiler-work counter, source/output +metric, and executable byte remains identical. + ## Next actions and decision boundary Authorized low-risk work: