diff --git a/components/salsa-macro-rules/src/setup_input_struct.rs b/components/salsa-macro-rules/src/setup_input_struct.rs index c6af89075..0e0ec6f84 100644 --- a/components/salsa-macro-rules/src/setup_input_struct.rs +++ b/components/salsa-macro-rules/src/setup_input_struct.rs @@ -135,6 +135,8 @@ macro_rules! setup_input_struct { } impl $zalsa::SalsaStructInDb for $Struct { + type MemoIngredientMap = $zalsa::MemoIngredientSingletonIndex; + fn lookup_or_create_ingredient_index(aux: &$zalsa::Zalsa) -> $zalsa::IngredientIndices { aux.add_or_lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>().into() } diff --git a/components/salsa-macro-rules/src/setup_interned_struct.rs b/components/salsa-macro-rules/src/setup_interned_struct.rs index 56ba9bbcc..668f444e1 100644 --- a/components/salsa-macro-rules/src/setup_interned_struct.rs +++ b/components/salsa-macro-rules/src/setup_interned_struct.rs @@ -171,6 +171,8 @@ macro_rules! setup_interned_struct { } impl< $($db_lt_arg)? > $zalsa::SalsaStructInDb for $Struct< $($db_lt_arg)? > { + type MemoIngredientMap = $zalsa::MemoIngredientSingletonIndex; + fn lookup_or_create_ingredient_index(aux: &$zalsa::Zalsa) -> $zalsa::IngredientIndices { aux.add_or_lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>().into() } diff --git a/components/salsa-macro-rules/src/setup_tracked_fn.rs b/components/salsa-macro-rules/src/setup_tracked_fn.rs index c2dad6fbd..71cd2751c 100644 --- a/components/salsa-macro-rules/src/setup_tracked_fn.rs +++ b/components/salsa-macro-rules/src/setup_tracked_fn.rs @@ -99,8 +99,10 @@ macro_rules! setup_tracked_fn { $zalsa::IngredientCache::new(); impl $zalsa::SalsaStructInDb for $InternedData<'_> { + type MemoIngredientMap = $zalsa::MemoIngredientSingletonIndex; + fn lookup_or_create_ingredient_index(aux: &$zalsa::Zalsa) -> $zalsa::IngredientIndices { - $zalsa::IngredientIndices::uninitialized() + $zalsa::IngredientIndices::empty() } #[inline] @@ -212,7 +214,7 @@ macro_rules! setup_tracked_fn { { $zalsa::macro_if! { if $needs_interner { - $zalsa::IngredientIndices::uninitialized() + $zalsa::IngredientIndices::empty() } else { <$InternedData as $zalsa::SalsaStructInDb>::lookup_or_create_ingredient_index(zalsa) } @@ -224,18 +226,18 @@ macro_rules! setup_tracked_fn { first_index: $zalsa::IngredientIndex, struct_index: $zalsa::IngredientIndices, ) -> Vec> { - let struct_index = $zalsa::macro_if! { + let struct_index: $zalsa::IngredientIndices = $zalsa::macro_if! { if $needs_interner { first_index.successor(0).into() } else { struct_index } }; + let memo_ingredient_indices = From::from((zalsa, struct_index, first_index)); let fn_ingredient = <$zalsa::function::IngredientImpl<$Configuration>>::new( - struct_index, first_index, - zalsa, + memo_ingredient_indices, ); fn_ingredient.set_capacity($lru); $zalsa::macro_if! { diff --git a/components/salsa-macro-rules/src/setup_tracked_struct.rs b/components/salsa-macro-rules/src/setup_tracked_struct.rs index 7cf01a04f..05563dac9 100644 --- a/components/salsa-macro-rules/src/setup_tracked_struct.rs +++ b/components/salsa-macro-rules/src/setup_tracked_struct.rs @@ -183,6 +183,8 @@ macro_rules! setup_tracked_struct { } impl $zalsa::SalsaStructInDb for $Struct<'_> { + type MemoIngredientMap = $zalsa::MemoIngredientSingletonIndex; + fn lookup_or_create_ingredient_index(aux: &$zalsa::Zalsa) -> $zalsa::IngredientIndices { aux.add_or_lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>().into() } diff --git a/components/salsa-macros/src/supertype.rs b/components/salsa-macros/src/supertype.rs index 610f9d1b4..c17e60319 100644 --- a/components/salsa-macros/src/supertype.rs +++ b/components/salsa-macros/src/supertype.rs @@ -70,15 +70,11 @@ fn enum_impl(enum_item: syn::ItemEnum) -> syn::Result { let salsa_struct_in_db = quote! { impl #impl_generics zalsa::SalsaStructInDb for #enum_name #type_generics #where_clause { + type MemoIngredientMap = zalsa::MemoIngredientIndices; + #[inline] fn lookup_or_create_ingredient_index(__zalsa: &zalsa::Zalsa) -> zalsa::IngredientIndices { - let mut __result = zalsa::IngredientIndices::uninitialized(); - #( - __result.merge( - &<#variant_types as zalsa::SalsaStructInDb>::lookup_or_create_ingredient_index(__zalsa) - ); - )* - __result + zalsa::IngredientIndices::merge([ #( <#variant_types as zalsa::SalsaStructInDb>::lookup_or_create_ingredient_index(__zalsa) ),* ]) } #[inline] diff --git a/src/function.rs b/src/function.rs index 8f9145e4c..179ad3dfa 100644 --- a/src/function.rs +++ b/src/function.rs @@ -5,7 +5,6 @@ use crate::{ cycle::CycleRecoveryStrategy, ingredient::{fmt_index, MaybeChangedAfter}, key::DatabaseKeyIndex, - memo_ingredient_indices::{IngredientIndices, MemoIngredientIndices}, salsa_struct::SalsaStructInDb, zalsa::{IngredientIndex, MemoIngredientIndex, Zalsa}, zalsa_local::QueryOrigin, @@ -95,7 +94,10 @@ pub struct IngredientImpl { index: IngredientIndex, /// The index for the memo/sync tables - memo_ingredient_indices: MemoIngredientIndices, + /// + /// This may be a `MemoIngredientSingletonIndex` or a `MemoIngredientIndex`, depending on + /// whether the tracked function's struct is a plain salsa struct or an enum `#[derive(Supertype)]`. + memo_ingredient_indices: as SalsaStructInDb>::MemoIngredientMap, /// Used to find memos to throw out when we have too many memoized values. lru: lru::Lru, @@ -126,9 +128,10 @@ impl IngredientImpl where C: Configuration, { - pub fn new(struct_indices: IngredientIndices, index: IngredientIndex, zalsa: &Zalsa) -> Self { - let memo_ingredient_indices = struct_indices - .memo_indices(|struct_index| zalsa.next_memo_ingredient_index(struct_index, index)); + pub fn new( + index: IngredientIndex, + memo_ingredient_indices: as SalsaStructInDb>::MemoIngredientMap, + ) -> Self { Self { index, memo_ingredient_indices, @@ -187,8 +190,7 @@ where #[inline] fn memo_ingredient_index(&self, zalsa: &Zalsa, id: Id) -> MemoIngredientIndex { - self.memo_ingredient_indices - .find(zalsa.ingredient_index(id)) + self.memo_ingredient_indices[zalsa.ingredient_index(id)] } } diff --git a/src/ingredient.rs b/src/ingredient.rs index 09f39044b..b63c7c8d0 100644 --- a/src/ingredient.rs +++ b/src/ingredient.rs @@ -26,7 +26,7 @@ pub trait Jar: Any { where Self: Sized, { - IngredientIndices::uninitialized() + IngredientIndices::empty() } /// Create the ingredients given the index of the first one. diff --git a/src/lib.rs b/src/lib.rs index ea124aaf5..28e702e1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,8 +52,8 @@ pub use salsa_macros::accumulator; pub use salsa_macros::db; pub use salsa_macros::input; pub use salsa_macros::interned; -pub use salsa_macros::tracked; pub use salsa_macros::supertype; +pub use salsa_macros::tracked; pub use salsa_macros::Update; pub mod prelude { @@ -87,7 +87,9 @@ pub mod plumbing { pub use crate::ingredient::Ingredient; pub use crate::ingredient::Jar; pub use crate::key::DatabaseKeyIndex; - pub use crate::memo_ingredient_indices::IngredientIndices; + pub use crate::memo_ingredient_indices::{ + IngredientIndices, MemoIngredientIndices, MemoIngredientSingletonIndex, + }; pub use crate::revision::Revision; pub use crate::runtime::stamp; pub use crate::runtime::Runtime; diff --git a/src/memo_ingredient_indices.rs b/src/memo_ingredient_indices.rs index 0c7e3bf8d..b9cc53d10 100644 --- a/src/memo_ingredient_indices.rs +++ b/src/memo_ingredient_indices.rs @@ -1,114 +1,122 @@ -use std::fmt; +use std::ops; -use crate::zalsa::MemoIngredientIndex; +use crate::zalsa::{MemoIngredientIndex, Zalsa}; use crate::IngredientIndex; -/// The maximum number of memo ingredient indices we can hold. This affects the -/// maximum number of variants possible in `#[derive(salsa::Enum)]`. We use a const -/// so that we don't allocate and to perhaps allow the compiler to vectorize the search. -pub const MAX_MEMO_INGREDIENT_INDICES: usize = 20; - /// An ingredient has an [ingredient index][IngredientIndex]. However, Salsa also supports -/// enums of salsa structs, and those don't have a constant ingredient index, because they -/// are not ingredients by themselves but rather composed of them. However, an enum can be -/// viewed as a *set* of [`IngredientIndex`], where each instance of the enum can belong +/// enums of salsa structs (and other salsa enums), and those don't have a constant ingredient index, +/// because they are not ingredients by themselves but rather composed of them. However, an enum can +/// be viewed as a *set* of [`IngredientIndex`], where each instance of the enum can belong /// to one, potentially different, index. This is what this type represents: a set of /// `IngredientIndex`. -/// -/// This type is represented as an array, for efficiency, and supports up to 20 indices. -/// That means that Salsa enums can have at most 20 variants. Alternatively, they can also -/// contain Salsa enums as variants, but then the total number of variants is counter - because -/// what matters is the number of unique `IngredientIndex`s. #[derive(Clone)] pub struct IngredientIndices { - indices: [IngredientIndex; MAX_MEMO_INGREDIENT_INDICES], - len: u8, + indices: Box<[IngredientIndex]>, } impl From for IngredientIndices { #[inline] fn from(value: IngredientIndex) -> Self { - let mut result = Self::uninitialized(); - result.indices[0] = value; - result.len = 1; - result - } -} - -impl fmt::Debug for IngredientIndices { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_list() - .entries(&self.indices[..self.len.into()]) - .finish() + Self { + indices: Box::new([value]), + } } } impl IngredientIndices { #[inline] - pub(crate) fn memo_indices( - &self, - mut memo_index: impl FnMut(IngredientIndex) -> MemoIngredientIndex, - ) -> MemoIngredientIndices { - let mut memo_ingredient_indices = [( - IngredientIndex::from((u32::MAX - 1) as usize), - MemoIngredientIndex::from_usize((u32::MAX - 1) as usize), - ); MAX_MEMO_INGREDIENT_INDICES]; - for i in 0..usize::from(self.len) { - let memo_ingredient_index = memo_index(self.indices[i]); - memo_ingredient_indices[i] = (self.indices[i], memo_ingredient_index); - } - MemoIngredientIndices { - indices: memo_ingredient_indices, - len: self.len, + pub fn empty() -> Self { + Self { + indices: Box::default(), } } - #[inline] - pub fn uninitialized() -> Self { + pub fn merge(iter: impl IntoIterator) -> Self { + let mut indices = Vec::new(); + for index in iter { + indices.extend(index.indices); + } + indices.sort_unstable(); + indices.dedup(); Self { - indices: [IngredientIndex::from((u32::MAX - 1) as usize); MAX_MEMO_INGREDIENT_INDICES], - len: 0, + indices: indices.into_boxed_slice(), } } +} - #[track_caller] +impl From<(&Zalsa, IngredientIndices, IngredientIndex)> for MemoIngredientIndices { #[inline] - pub fn merge(&mut self, other: &Self) { - if usize::from(self.len) + usize::from(other.len) > MAX_MEMO_INGREDIENT_INDICES { - panic!("too many variants in the salsa enum"); + fn from( + (zalsa, struct_indices, ingredient): (&Zalsa, IngredientIndices, IngredientIndex), + ) -> Self { + let Some(&last) = struct_indices.indices.last() else { + unreachable!("Attempting to construct struct memo mapping for non tracked function?") + }; + let mut indices = Vec::new(); + indices.resize( + last.as_usize() + 1, + MemoIngredientIndex::from_usize((u32::MAX - 1) as usize), + ); + for &struct_ingredient in &struct_indices.indices { + indices[struct_ingredient.as_usize()] = + zalsa.next_memo_ingredient_index(struct_ingredient, ingredient); + } + MemoIngredientIndices { + indices: indices.into_boxed_slice(), } - self.indices[usize::from(self.len)..][..usize::from(other.len)] - .copy_from_slice(&other.indices[..usize::from(other.len)]); - self.len += other.len; } } /// This type is to [`MemoIngredientIndex`] what [`IngredientIndices`] is to [`IngredientIndex`]: /// since enums can contain different ingredient indices, they can also have different memo indices, /// so we need to keep track of them. -#[derive(Clone)] +/// +/// This acts a map from [`IngredientIndex`] to [`MemoIngredientIndex`] but implemented +/// via a slice for fast lookups, trading memory for speed. With these changes, lookups are `O(1)` +/// instead of `O(n)`. +/// +/// A database tends to have few ingredients (i), less function ingredients and even less +/// function ingredients targeting `#[derive(Supertype)]` enums (e). +/// While this is bounded as `O(i * e)` memory usage, the average case is significantly smaller: a +/// function ingredient targeting enums only stores a slice whose length corresponds to the largest +/// ingredient index's _value_. For example, if we have the ingredient indices `[2, 6, 17]`, then we +/// will allocate a slice whose length is `17 + 1`. +/// +/// Assuming a heavy example scenario of 1000 ingredients (500 of which are function ingredients, 100 +/// of which are enum targeting functions) this would come out to a maximum possibly memory usage of +/// 4bytes * 1000 * 100 ~= 0.38MB which is negligible. pub struct MemoIngredientIndices { - indices: [(IngredientIndex, MemoIngredientIndex); MAX_MEMO_INGREDIENT_INDICES], - len: u8, + indices: Box<[MemoIngredientIndex]>, } -impl fmt::Debug for MemoIngredientIndices { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_list() - .entries(&self.indices[..self.len.into()]) - .finish() +impl ops::Index for MemoIngredientIndices { + type Output = MemoIngredientIndex; + + #[inline] + fn index(&self, index: IngredientIndex) -> &Self::Output { + &self.indices[index.as_usize()] } } -impl MemoIngredientIndices { +#[derive(Debug)] +pub struct MemoIngredientSingletonIndex(MemoIngredientIndex); + +impl ops::Index for MemoIngredientSingletonIndex { + type Output = MemoIngredientIndex; + #[inline] - pub(crate) fn find(&self, ingredient_index: IngredientIndex) -> MemoIngredientIndex { - for &(ingredient, memo_ingredient_index) in &self.indices[..(self.len - 1).into()] { - if ingredient == ingredient_index { - return memo_ingredient_index; - } - } - // It must be the last. - self.indices[usize::from(self.len - 1)].1 + fn index(&self, _: IngredientIndex) -> &Self::Output { + &self.0 + } +} + +impl From<(&Zalsa, IngredientIndices, IngredientIndex)> for MemoIngredientSingletonIndex { + #[inline] + fn from((zalsa, indices, ingredient): (&Zalsa, IngredientIndices, IngredientIndex)) -> Self { + let &[struct_ingredient] = &*indices.indices else { + unreachable!("Attempting to construct struct memo mapping from enum?") + }; + + Self(zalsa.next_memo_ingredient_index(struct_ingredient, ingredient)) } } diff --git a/src/salsa_struct.rs b/src/salsa_struct.rs index bba9fd34b..493d09d42 100644 --- a/src/salsa_struct.rs +++ b/src/salsa_struct.rs @@ -5,6 +5,10 @@ use crate::zalsa::Zalsa; use crate::Id; pub trait SalsaStructInDb: Sized { + type MemoIngredientMap: std::ops::Index + + Send + + Sync; + /// This method is used to create ingredient indices. Note, it does *not* create the ingredients /// themselves, that is the job of [`Zalsa::add_or_lookup_jar_by_type()`]. This method only creates /// or lookup the indices. Naturally, implementors may call `add_or_lookup_jar_by_type()` to diff --git a/tests/interned-structs_self_ref.rs b/tests/interned-structs_self_ref.rs index bd6f0e5d6..272e21667 100644 --- a/tests/interned-structs_self_ref.rs +++ b/tests/interned-structs_self_ref.rs @@ -112,6 +112,8 @@ const _: () = { } } impl zalsa_::SalsaStructInDb for InternedString<'_> { + type MemoIngredientMap = zalsa_::MemoIngredientSingletonIndex; + fn lookup_or_create_ingredient_index(aux: &Zalsa) -> salsa::plumbing::IngredientIndices { aux.add_or_lookup_jar_by_type::>() .into()