Skip to content

Commit 4c10d32

Browse files
Support a new Durability::NEVER_CHANGE
Inputs with this durability are assumed to never change. This allows us to optimize their storage a lot and not store any data about them except for the value. That makes them very compact. This is done by bit-tagging the memo and using it as one of two types. The overhead of handling the possibility of a second type will be seen in synthetic benchmarks; in practice, if using this durability (for example, for library code), speed will increase, not decrease (in addition to improved memory usage), because we don't need to validate query dependencies if they are assumed to never change. Values participating in cycles cannot have `Durability::NEVER_CHANGE`: while it is possible to support this (and in fact, an early revision of the code did) it introduces a lot of complications to the code, as we need to replace the type of a memo after it has been inserted.
1 parent 51c5bc4 commit 4c10d32

24 files changed

+695
-217
lines changed

components/salsa-macro-rules/src/setup_input_struct.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ macro_rules! setup_input_struct {
392392
pub(super) fn new_builder($($field_id: $field_ty),*) -> $Builder {
393393
$Builder {
394394
fields: ($($field_id,)*),
395-
durabilities: [::salsa::Durability::default(); $N],
395+
durabilities: [::salsa::Durability::MIN; $N],
396396
}
397397
}
398398

components/salsa-macro-rules/src/setup_tracked_fn.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ macro_rules! setup_tracked_fn {
391391
zalsa,
392392
struct_index,
393393
first_index,
394-
$zalsa::function::MemoEntryType::of::<$zalsa::function::Memo<$Configuration>>(),
394+
$zalsa::function::MemoEntryType::of::<$zalsa::function::AmbiguousMemo<$Configuration>>(),
395395
intern_ingredient_memo_types,
396396
)
397397
};

src/accumulator/accumulated_map.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ impl std::fmt::Debug for AccumulatedMap {
2121
}
2222

2323
impl AccumulatedMap {
24+
pub(crate) const EMPTY: AccumulatedMap = AccumulatedMap {
25+
map: hashbrown::HashMap::with_hasher(FxBuildHasher),
26+
};
27+
2428
pub fn accumulate<A: Accumulator>(&mut self, index: IngredientIndex, value: A) {
2529
self.map
2630
.entry(index)

src/active_query.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ impl ActiveQuery {
106106
) {
107107
self.durability = self.durability.min(durability);
108108
self.changed_at = self.changed_at.max(changed_at);
109-
self.input_outputs.insert(QueryEdge::input(input));
110109
self.cycle_heads.extend(cycle_heads);
111110
#[cfg(feature = "accumulator")]
112111
{
@@ -115,6 +114,10 @@ impl ActiveQuery {
115114
false => accumulated_inputs.load(),
116115
});
117116
}
117+
if !cycle_heads.is_empty() || durability != Durability::NEVER_CHANGE {
118+
// During cycles we need to record dependencies.
119+
self.input_outputs.insert(QueryEdge::input(input));
120+
}
118121
}
119122

120123
pub(super) fn add_read_simple(
@@ -125,7 +128,9 @@ impl ActiveQuery {
125128
) {
126129
self.durability = self.durability.min(durability);
127130
self.changed_at = self.changed_at.max(revision);
128-
self.input_outputs.insert(QueryEdge::input(input));
131+
if durability != Durability::NEVER_CHANGE {
132+
self.input_outputs.insert(QueryEdge::input(input));
133+
}
129134
}
130135

131136
pub(super) fn add_untracked_read(&mut self, changed_at: Revision) {
@@ -147,7 +152,9 @@ impl ActiveQuery {
147152

148153
/// Adds a key to our list of outputs.
149154
pub(super) fn add_output(&mut self, key: DatabaseKeyIndex) {
150-
self.input_outputs.insert(QueryEdge::output(key));
155+
if self.durability != Durability::NEVER_CHANGE {
156+
self.input_outputs.insert(QueryEdge::output(key));
157+
}
151158
}
152159

153160
/// True if the given key was output by this query.

src/cycle.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ impl<'de> serde::Deserialize<'de> for AtomicIterationCount {
217217
pub struct CycleHeads(ThinVec<CycleHead>);
218218

219219
impl CycleHeads {
220+
#[inline]
220221
pub(crate) fn is_empty(&self) -> bool {
221222
self.0.is_empty()
222223
}
@@ -505,6 +506,7 @@ pub enum ProvisionalStatus<'db> {
505506
iteration: IterationCount,
506507
verified_at: Revision,
507508
},
509+
FinalNeverChange,
508510
FallbackImmediate,
509511
}
510512

src/durability.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ impl std::fmt::Debug for Durability {
4646
DurabilityVal::Low => f.write_str("Durability::LOW"),
4747
DurabilityVal::Medium => f.write_str("Durability::MEDIUM"),
4848
DurabilityVal::High => f.write_str("Durability::HIGH"),
49+
DurabilityVal::NeverChange => f.write_str("Durability::NEVER_CHANGE"),
4950
}
5051
} else {
5152
f.debug_tuple("Durability")
@@ -61,14 +62,17 @@ enum DurabilityVal {
6162
Low = 0,
6263
Medium = 1,
6364
High = 2,
65+
NeverChange = 3,
6466
}
6567

6668
impl From<u8> for DurabilityVal {
69+
#[inline]
6770
fn from(value: u8) -> Self {
6871
match value {
6972
0 => DurabilityVal::Low,
7073
1 => DurabilityVal::Medium,
7174
2 => DurabilityVal::High,
75+
3 => DurabilityVal::NeverChange,
7276
_ => panic!("invalid durability"),
7377
}
7478
}
@@ -87,23 +91,28 @@ impl Durability {
8791

8892
/// High durability: things that are not expected to change under
8993
/// common usage.
90-
///
91-
/// Example: the standard library or something from crates.io
9294
pub const HIGH: Durability = Durability(DurabilityVal::High);
9395

96+
/// The input is guaranteed to never change. Queries calling it won't have
97+
/// it as a dependency.
98+
///
99+
/// Example: the standard library or something from crates.io.
100+
pub const NEVER_CHANGE: Durability = Durability(DurabilityVal::NeverChange);
101+
94102
/// The minimum possible durability; equivalent to LOW but
95103
/// "conceptually" distinct (i.e., if we add more durability
96104
/// levels, this could change).
97-
pub(crate) const MIN: Durability = Self::LOW;
105+
pub const MIN: Durability = Self::LOW;
98106

99-
/// The maximum possible durability; equivalent to HIGH but
107+
/// The maximum possible durability; equivalent to NEVER_CHANGE but
100108
/// "conceptually" distinct (i.e., if we add more durability
101109
/// levels, this could change).
102-
pub(crate) const MAX: Durability = Self::HIGH;
110+
pub(crate) const MAX: Durability = Self::NEVER_CHANGE;
103111

104112
/// Number of durability levels.
105-
pub(crate) const LEN: usize = Self::HIGH.0 as usize + 1;
113+
pub(crate) const LEN: usize = Self::MAX.0 as usize + 1;
106114

115+
#[inline]
107116
pub(crate) fn index(self) -> usize {
108117
self.0 as usize
109118
}

src/function.rs

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ use crate::key::DatabaseKeyIndex;
1616
use crate::plumbing::{self, MemoIngredientMap};
1717
use crate::salsa_struct::SalsaStructInDb;
1818
use crate::sync::Arc;
19-
use crate::table::memo::MemoTableTypes;
19+
use crate::table::memo::{Either, MemoTableTypes};
2020
use crate::table::Table;
2121
use crate::views::DatabaseDownCaster;
2222
use crate::zalsa::{IngredientIndex, JarKind, MemoIngredientIndex, Zalsa};
2323
use crate::zalsa_local::{QueryEdge, QueryOriginRef};
2424
use crate::{Cycle, Durability, Id, Revision};
2525

26+
pub use crate::function::memo::AmbiguousMemo;
27+
2628
#[cfg(feature = "accumulator")]
2729
mod accumulated;
2830
mod backdate;
@@ -37,7 +39,9 @@ mod memo;
3739
mod specify;
3840
mod sync;
3941

40-
pub type Memo<C> = memo::Memo<'static, C>;
42+
type EitherMemoNonNull<'db, C> =
43+
Either<NonNull<memo::Memo<'db, C>>, NonNull<memo::NeverChangeMemo<'db, C>>>;
44+
type EitherMemoRef<'a, 'db, C> = Either<&'a memo::Memo<'db, C>, &'a memo::NeverChangeMemo<'db, C>>;
4145

4246
pub trait Configuration: Any {
4347
const DEBUG_NAME: &'static str;
@@ -246,6 +250,7 @@ where
246250
/// when this function is called and (b) ensuring that any entries
247251
/// removed from the memo-map are added to `deleted_entries`, which is
248252
/// only cleared with `&mut self`.
253+
#[inline]
249254
unsafe fn extend_memo_lifetime<'this>(
250255
&'this self,
251256
memo: &memo::Memo<'this, C>,
@@ -254,6 +259,15 @@ where
254259
unsafe { std::mem::transmute(memo) }
255260
}
256261

262+
#[inline]
263+
unsafe fn extend_either_memo_lifetime<'this>(
264+
&'this self,
265+
memo: EitherMemoRef<'_, 'this, C>,
266+
) -> EitherMemoRef<'this, 'this, C> {
267+
// SAFETY: the caller must guarantee that the memo will not be released before `&self`
268+
unsafe { std::mem::transmute(memo) }
269+
}
270+
257271
fn insert_memo<'db>(
258272
&'db self,
259273
zalsa: &'db Zalsa,
@@ -284,6 +298,39 @@ where
284298
unsafe { self.extend_memo_lifetime(memo.as_ref()) }
285299
}
286300

301+
fn insert_never_change_memo<'db>(
302+
&'db self,
303+
zalsa: &'db Zalsa,
304+
id: Id,
305+
memo: memo::NeverChangeMemo<'db, C>,
306+
memo_ingredient_index: MemoIngredientIndex,
307+
) -> EitherMemoRef<'db, 'db, C> {
308+
// We convert to a `NonNull` here as soon as possible because we are going to alias
309+
// into the `Box`, which is a `noalias` type.
310+
// SAFETY: memo is not null
311+
let memo = unsafe { NonNull::new_unchecked(Box::into_raw(Box::new(memo))) };
312+
313+
// SAFETY: memo must be in the map (it's not yet, but it will be by the time this
314+
// value is returned) and anything removed from map is added to deleted entries (ensured elsewhere).
315+
let db_memo = unsafe { self.extend_either_memo_lifetime(Either::Right(memo.as_ref())) };
316+
317+
if let Some(old_value) =
318+
// SAFETY: We delay the drop of `old_value` until a new revision starts which ensures no
319+
// references will exist for the memo contents.
320+
unsafe {
321+
self.insert_never_change_memo_into_table_for(zalsa, id, memo, memo_ingredient_index)
322+
}
323+
{
324+
// In case there is a reference to the old memo out there, we have to store it
325+
// in the deleted entries. This will get cleared when a new revision starts.
326+
//
327+
// SAFETY: Once the revision starts, there will be no outstanding borrows to the
328+
// memo contents, and so it will be safe to free.
329+
unsafe { self.deleted_entries.push(old_value) };
330+
}
331+
db_memo
332+
}
333+
287334
#[inline]
288335
fn memo_ingredient_index(&self, zalsa: &Zalsa, id: Id) -> MemoIngredientIndex {
289336
self.memo_ingredient_indices.get_zalsa_id(zalsa, id)
@@ -335,6 +382,7 @@ where
335382
else {
336383
return;
337384
};
385+
let Either::Left(memo) = memo else { todo!() };
338386

339387
let origin = memo.revisions.origin.as_ref();
340388

@@ -371,8 +419,11 @@ where
371419
zalsa: &'db Zalsa,
372420
input: Id,
373421
) -> Option<ProvisionalStatus<'db>> {
374-
let memo =
375-
self.get_memo_from_table_for(zalsa, input, self.memo_ingredient_index(zalsa, input))?;
422+
let Either::Left(memo) =
423+
self.get_memo_from_table_for(zalsa, input, self.memo_ingredient_index(zalsa, input))?
424+
else {
425+
return Some(ProvisionalStatus::FinalNeverChange);
426+
};
376427

377428
let iteration = memo.revisions.iteration();
378429
let verified_final = memo.revisions.verified_final.load(Ordering::Relaxed);
@@ -396,7 +447,7 @@ where
396447
}
397448

398449
fn set_cycle_iteration_count(&self, zalsa: &Zalsa, input: Id, iteration_count: IterationCount) {
399-
let Some(memo) =
450+
let Some(Either::Left(memo)) =
400451
self.get_memo_from_table_for(zalsa, input, self.memo_ingredient_index(zalsa, input))
401452
else {
402453
return;
@@ -407,7 +458,7 @@ where
407458
}
408459

409460
fn finalize_cycle_head(&self, zalsa: &Zalsa, input: Id) {
410-
let Some(memo) =
461+
let Some(Either::Left(memo)) =
411462
self.get_memo_from_table_for(zalsa, input, self.memo_ingredient_index(zalsa, input))
412463
else {
413464
return;
@@ -417,7 +468,7 @@ where
417468
}
418469

419470
fn cycle_converged(&self, zalsa: &Zalsa, input: Id) -> bool {
420-
let Some(memo) =
471+
let Some(Either::Left(memo)) =
421472
self.get_memo_from_table_for(zalsa, input, self.memo_ingredient_index(zalsa, input))
422473
else {
423474
return true;
@@ -534,7 +585,12 @@ where
534585
let memo =
535586
self.get_memo_from_table_for(zalsa, entry.key_index(), memo_ingredient_index);
536587

537-
if memo.is_some_and(|memo| memo.should_serialize()) {
588+
let should_serialize = match memo {
589+
Some(Either::Left(memo)) => memo.should_serialize(),
590+
Some(Either::Right(memo)) => memo.should_serialize(),
591+
None => false,
592+
};
593+
if should_serialize {
538594
return true;
539595
}
540596
}

src/function/accumulated.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::accumulator::accumulated_map::{AccumulatedMap, InputAccumulatedValues
22
use crate::accumulator::{self};
33
use crate::function::{Configuration, IngredientImpl};
44
use crate::hash::FxHashSet;
5+
use crate::table::memo::Either;
56
use crate::zalsa::ZalsaDatabase;
67
use crate::zalsa_local::QueryOriginRef;
78
use crate::{DatabaseKeyIndex, Id};
@@ -100,9 +101,15 @@ where
100101
let (zalsa, zalsa_local) = db.zalsas();
101102
// NEXT STEP: stash and refactor `fetch` to return an `&Memo` so we can make this work
102103
let memo = self.refresh_memo(db, zalsa, zalsa_local, key);
103-
(
104-
memo.revisions.accumulated(),
105-
memo.revisions.accumulated_inputs.load(),
106-
)
104+
match memo {
105+
Either::Left(memo) => (
106+
memo.revisions.accumulated(),
107+
memo.revisions.accumulated_inputs.load(),
108+
),
109+
Either::Right(_) => (
110+
Some(const { &AccumulatedMap::EMPTY }),
111+
InputAccumulatedValues::Empty,
112+
),
113+
}
107114
}
108115
}

src/function/delete.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use std::ptr::NonNull;
22

3-
use crate::function::memo::Memo;
4-
use crate::function::Configuration;
3+
use crate::function::memo::{Memo, NeverChangeMemo};
4+
use crate::function::{Configuration, EitherMemoNonNull};
5+
use crate::table::memo::Either;
56

67
/// Stores the list of memos that have been deleted so they can be freed
78
/// once the next revision starts. See the comment on the field
89
/// `deleted_entries` of [`FunctionIngredient`][] for more details.
910
pub(super) struct DeletedEntries<C: Configuration> {
10-
memos: boxcar::Vec<SharedBox<Memo<'static, C>>>,
11+
memos: boxcar::Vec<Either<SharedBox<Memo<'static, C>>, SharedBox<NeverChangeMemo<'static, C>>>>,
1112
}
1213

1314
#[allow(clippy::undocumented_unsafe_blocks)] // TODO(#697) document safety
@@ -27,12 +28,17 @@ impl<C: Configuration> DeletedEntries<C> {
2728
/// # Safety
2829
///
2930
/// The memo must be valid and safe to free when the `DeletedEntries` list is cleared or dropped.
30-
pub(super) unsafe fn push(&self, memo: NonNull<Memo<'_, C>>) {
31+
pub(super) unsafe fn push(&self, memo: EitherMemoNonNull<'_, C>) {
3132
// Safety: The memo must be valid and safe to free when the `DeletedEntries` list is cleared or dropped.
32-
let memo =
33-
unsafe { std::mem::transmute::<NonNull<Memo<'_, C>>, NonNull<Memo<'static, C>>>(memo) };
34-
35-
self.memos.push(SharedBox(memo));
33+
let memo = unsafe {
34+
std::mem::transmute::<EitherMemoNonNull<'_, C>, EitherMemoNonNull<'static, C>>(memo)
35+
};
36+
37+
let memo = match memo {
38+
Either::Left(it) => Either::Left(SharedBox(it)),
39+
Either::Right(it) => Either::Right(SharedBox(it)),
40+
};
41+
self.memos.push(memo);
3642
}
3743

3844
/// Free all deleted memos, keeping the list available for reuse.

0 commit comments

Comments
 (0)