Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions benches/eviction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ fn main() {
/// small per-hit costs can add up. Every result remains resident, which isolates
/// hit-path bookkeeping. `NoEviction` shows the tracked-query cost without an
/// eviction policy.
#[divan::bench(args = [Policy::NoEviction, Policy::Lru])]
#[divan::bench(args = [Policy::NoEviction, Policy::Lru, Policy::Sieve])]
fn fast_path(bencher: divan::Bencher, policy: Policy) {
const ITEMS: usize = 1_024;

Expand All @@ -59,7 +59,7 @@ fn fast_path(bencher: divan::Bencher, policy: Policy) {
/// tracked function. This measures the remaining disabled-policy branch on the
/// hit path. `NoEviction` is the policy-free reference for how cheap this path
/// could be.
#[divan::bench(args = [Policy::NoEviction, Policy::Lru])]
#[divan::bench(args = [Policy::NoEviction, Policy::Lru, Policy::Sieve])]
fn disabled_eviction(bencher: divan::Bencher, policy: Policy) {
const ITEMS: usize = 1_024;

Expand All @@ -82,7 +82,7 @@ fn disabled_eviction(bencher: divan::Bencher, policy: Policy) {
/// reads a fully resident working set and then triggers a sweep, separating
/// steady-state sweep bookkeeping from victim selection and destruction.
#[divan::bench(
args = [Policy::NoEviction, Policy::Lru],
args = [Policy::NoEviction, Policy::Lru, Policy::Sieve],
sample_count = 20,
sample_size = 1
)]
Expand All @@ -99,7 +99,7 @@ fn fast_path_and_sweep(bencher: divan::Bencher, policy: Policy) {
})
.bench_local_refs(|(db, items)| {
let result = access_all(policy, &*db, items.iter().copied());
db.trigger_lru_eviction();
db.trigger_eviction();
black_box(result)
});
}
Expand All @@ -116,7 +116,7 @@ fn fast_path_and_sweep(bencher: divan::Bencher, policy: Policy) {
/// selection, and value destruction. `NoEviction` provides the computation
/// baseline; setup and final database destruction remain outside timing.
#[divan::bench(
args = [Policy::NoEviction, Policy::Lru],
args = [Policy::NoEviction, Policy::Lru, Policy::Sieve],
sample_count = 20,
sample_size = 1
)]
Expand All @@ -133,7 +133,7 @@ fn fill_and_evict(bencher: divan::Bencher, policy: Policy) {
})
.bench_local_refs(|(db, items)| {
let result = access_all(policy, &*db, items.iter().copied());
db.trigger_lru_eviction();
db.trigger_eviction();
black_box(result)
});
}
13 changes: 11 additions & 2 deletions benches/eviction/support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,29 @@ pub(crate) fn no_eviction_value(db: &dyn salsa::Database, item: Item) -> Value {
compute_value(item.value(db))
}

#[salsa::tracked(returns(copy), lru = 4096)]
#[salsa::tracked(returns(copy), eviction(policy = lru, capacity = 4096))]
pub(crate) fn lru_value(db: &dyn salsa::Database, item: Item) -> Value {
compute_value(item.value(db))
}

#[salsa::tracked(returns(copy), eviction(policy = sieve, capacity = 4096))]
pub(crate) fn sieve_value(db: &dyn salsa::Database, item: Item) -> Value {
compute_value(item.value(db))
}

#[derive(Clone, Copy)]
pub(crate) enum Policy {
NoEviction,
Lru,
Sieve,
}

impl Policy {
pub(crate) fn set_capacity(self, db: &mut salsa::DatabaseImpl, capacity: usize) {
match self {
Self::NoEviction => {}
Self::Lru => lru_value::set_lru_capacity(db, capacity),
Self::Lru => lru_value::set_eviction_capacity(db, capacity),
Self::Sieve => sieve_value::set_eviction_capacity(db, capacity),
}
}
}
Expand All @@ -39,6 +46,7 @@ impl std::fmt::Display for Policy {
formatter.write_str(match self {
Self::NoEviction => "NoEviction",
Self::Lru => "Lru",
Self::Sieve => "Sieve",
})
}
}
Expand All @@ -52,6 +60,7 @@ pub(crate) fn access_all(
match policy {
Policy::NoEviction => access_all_with(db, items, no_eviction_value),
Policy::Lru => access_all_with(db, items, lru_value),
Policy::Sieve => access_all_with(db, items, sieve_value),
}
}

Expand Down
14 changes: 8 additions & 6 deletions benches/eviction_walltime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ mod support;

use support::{
Item, Policy, Value, access_all, access_all_with, lru_value, new_items, no_eviction_value,
prewarm,
prewarm, sieve_value,
};

fn main() {
Expand Down Expand Up @@ -64,7 +64,7 @@ fn main() {
/// active entries that did not survive and how quickly the policy stabilizes
/// around the smaller working set.
#[divan::bench(
args = [Policy::NoEviction, Policy::Lru],
args = [Policy::NoEviction, Policy::Lru, Policy::Sieve],
sample_count = 20,
sample_size = 1
)]
Expand Down Expand Up @@ -125,7 +125,7 @@ fn project_check_then_incremental(bencher: divan::Bencher, policy: Policy) {
/// recurring-only round charges for entries displaced by the last scan.
/// `NoEviction` shows the cost when recurring values are never displaced.
#[divan::bench(
args = [Policy::NoEviction, Policy::Lru],
args = [Policy::NoEviction, Policy::Lru, Policy::Sieve],
sample_count = 20,
sample_size = 1
)]
Expand Down Expand Up @@ -201,7 +201,7 @@ fn scan_resistance(bencher: divan::Bencher, policy: Policy) {
/// provides the computation baseline; retaining cold entries cannot create hits
/// because they are never accessed again.
#[divan::bench(
args = [Policy::NoEviction, Policy::Lru],
args = [Policy::NoEviction, Policy::Lru, Policy::Sieve],
sample_count = 20,
sample_size = 1
)]
Expand Down Expand Up @@ -256,7 +256,7 @@ fn one_hit_wonders(bencher: divan::Bencher, policy: Policy) {
/// the policy-free baseline: it retains both phases, but only the second phase
/// is accessed after the switch.
#[divan::bench(
args = [Policy::NoEviction, Policy::Lru],
args = [Policy::NoEviction, Policy::Lru, Policy::Sieve],
sample_count = 20,
sample_size = 1
)]
Expand Down Expand Up @@ -295,7 +295,7 @@ fn phase_change(bencher: divan::Bencher, policy: Policy) {
/// misses and contention on the query itself. This isolates contention in the
/// eviction policy's hit bookkeeping; `NoEviction` provides the baseline.
#[divan::bench(
args = [Policy::NoEviction, Policy::Lru],
args = [Policy::NoEviction, Policy::Lru, Policy::Sieve],
sample_count = 20,
sample_size = 1
)]
Expand Down Expand Up @@ -349,6 +349,7 @@ impl Workload {
match policy {
Policy::NoEviction => self.run_with(no_eviction_value),
Policy::Lru => self.run_with(lru_value),
Policy::Sieve => self.run_with(sieve_value),
}
}

Expand All @@ -373,6 +374,7 @@ fn parallel_access_repeated(
parallel_access_repeated_with(jobs, accesses_per_item, no_eviction_value)
}
Policy::Lru => parallel_access_repeated_with(jobs, accesses_per_item, lru_value),
Policy::Sieve => parallel_access_repeated_with(jobs, accesses_per_item, sieve_value),
}
}

Expand Down
2 changes: 1 addition & 1 deletion book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
- [Durability](./plumbing/terminology/durability.md)
- [Input query](./plumbing/terminology/input_query.md)
- [Ingredient](./plumbing/terminology/ingredient.md)
- [LRU](./plumbing/terminology/LRU.md)
- [Eviction](./plumbing/terminology/eviction.md)
- [Memo](./plumbing/terminology/memo.md)
- [Query](./plumbing/terminology/query.md)
- [Query function](./plumbing/terminology/query_function.md)
Expand Down
1 change: 0 additions & 1 deletion book/src/plumbing/maybe_changed_after.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,3 @@ The logic for derived queries is more complex. We summarize the high-level ideas
[revision]: ./terminology/revision.md
[verified]: ./terminology/verified.md
[fetch]: ./fetch.md
[LRU]: ./terminology/LRU.md
6 changes: 0 additions & 6 deletions book/src/plumbing/terminology/LRU.md

This file was deleted.

31 changes: 31 additions & 0 deletions book/src/plumbing/terminology/eviction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Eviction

The `eviction` option on `#[salsa::tracked]` bounds the number of memoized values
retained for that function. The policy defaults to [SIEVE]. To use LRU instead,
set `policy = lru`.

SIEVE is recommended because its lock-free cache-hit path supports higher
throughput under concurrency without serializing on the eviction policy. The
[SIEVE paper] also found miss ratios comparable to or better than more complex
eviction policies. SIEVE can favor repeatedly accessed entries over one-hit
wonders, but tracks approximate rather than exact recency and may inspect
several residents when choosing a victim. Results remain workload-dependent.

LRU maintains exact recency. Salsa's current implementation guards that order
with an exclusive mutex. Every query access, including a cache hit, takes the
mutex and updates the order, so concurrent hits to otherwise independent entries
serialize.

A configured function generates
[`function_name::set_eviction_capacity(&mut db, capacity)`](https://docs.rs/salsa/latest/salsa/attr.tracked.html)
to adjust its capacity at runtime.

Eviction drops values from older [memos] to conserve memory, but retains their
[dependency] information so Salsa can still determine whether dependent values
may have changed. See [cache eviction] for configuration examples.

[cache eviction]: ../../tuning.md#cache-eviction
[dependency]: ./dependency.md
[memos]: ./memo.md
[SIEVE]: https://cachemon.github.io/SIEVE-website/
[SIEVE paper]: https://www.usenix.org/conference/nsdi24/presentation/zhang-yazhuo
4 changes: 2 additions & 2 deletions book/src/plumbing/terminology/memo.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A *memo* stores information about the last time that a [query function] for some [query] Q was executed:

* Typically, it contains the value that was returned from that function, so that we don't have to execute it again.
* However, this is not always true: some queries don't cache their result values, and values can also be dropped as a result of [LRU] collection. In those cases, the memo just stores [dependency] information, which can still be useful to determine if other queries that have Q as a [dependency] may have changed.
* However, this is not always true: some queries don't cache their result values, and an [eviction policy] can also drop values. In those cases, the memo just stores [dependency] information, which can still be useful to determine if other queries that have Q as a [dependency] may have changed.
* The revision in which the memo last [verified].
* The [changed at] revision in which the memo's value last changed. (Note that it may be [backdated].)
* The minimum durability of the memo's [dependencies].
Expand All @@ -18,4 +18,4 @@ A *memo* stores information about the last time that a [query function] for some
[query]: ./query.md
[query function]: ./query_function.md
[changed at]: ./changed_at.md
[LRU]: ./LRU.md
[eviction policy]: ./eviction.md
53 changes: 34 additions & 19 deletions book/src/tuning.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,65 @@
# Tuning Salsa

## Cache Eviction (LRU)
## Cache eviction

Salsa supports Least Recently Used (LRU) cache eviction for tracked functions.
By default, memoized values are never evicted (unbounded cache). You can enable
LRU eviction by specifying a capacity at compile time:
By default, tracked functions retain every memoized value for as long as its key
remains in the database. Configure an eviction policy to bound the number of
values retained by a function:

```rust
#[salsa::tracked(lru = 128)]
#[salsa::tracked(eviction(policy = sieve, capacity = 128))]
fn parse(db: &dyn Db, input: SourceFile) -> Ast {
// ...
}
```

With `lru = 128`, Salsa will keep at most 128 memoized values for this function.
When the cache exceeds this capacity, the least recently used values are evicted
at the start of each new revision.
With `capacity = 128`, Salsa will keep at most 128 memoized values for this
function. When the cache exceeds this capacity, Salsa uses the configured policy
to choose which values to evict at the start of the next revision. The optional
`policy` defaults to `sieve`. Salsa supports two policies:

- `sieve` is recommended. Its lock-free cache-hit path avoids serializing
concurrent accesses, and it often achieves better cache efficiency (a lower
miss ratio) than LRU.
- `lru` maintains exact least-recently-used order, but takes an exclusive lock on
every access.

See [eviction policies] for their behavior and tradeoffs.

Eviction drops memoized values while retaining query keys and dependency
metadata. A later access can therefore recompute an evicted value, while queries
that depend on it can still determine whether their own values may have changed.

[eviction policies]: ./plumbing/terminology/eviction.md

### Zero-Cost When Disabled

When no `lru` capacity is specified (the default), Salsa uses a no-op eviction
policy that is completely optimized away by the compiler. This means there is
zero runtime overhead for functions that don't need cache eviction.
When `eviction` is absent, Salsa uses a no-op policy that is optimized away by
the compiler. Functions without cache eviction therefore have no eviction-policy
bookkeeping.

### Runtime Capacity Adjustment

For functions with LRU enabled, you can adjust the capacity at runtime:
For functions with eviction configured, you can adjust the capacity at runtime:

```rust
#[salsa::tracked(lru = 128)]
#[salsa::tracked(eviction(capacity = 128))]
fn my_query(db: &dyn Db, input: MyInput) -> Output {
// ...
}

// Later, adjust the capacity:
my_query::set_lru_capacity(&mut db, 256);
my_query::set_eviction_capacity(&mut db, 256);
```

**Note:** The `set_lru_capacity` method is only generated for functions that have
an `lru` attribute. Functions without LRU enabled do not have this method.
The `set_eviction_capacity` method is only generated for functions with an
`eviction` option. Setting the capacity to zero disables eviction by that policy.

### Memory Management

LRU evicts memoized values, not query keys or dependency metadata. Salsa also
reclaims stale tracked outputs and unused low-durability interned values. Input
identities remain until the database is dropped.
Eviction removes memoized values, not query keys or dependency metadata. Salsa
also reclaims stale tracked outputs and unused low-durability interned values.
Input identities remain until the database is dropped.

## Intern Queries

Expand Down
12 changes: 6 additions & 6 deletions components/salsa-macro-rules/src/setup_tracked_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ macro_rules! setup_tracked_fn {
// The eviction policy type for this function
eviction: $Eviction:ty,

// LRU capacity (a literal, maybe 0)
lru: $lru:tt,
// Eviction capacity (a literal, or 0 when eviction is not configured)
eviction_capacity: $eviction_capacity:tt,

// The return mode for the function, see `salsa_macros::options::Option::returns`
return_mode: $return_mode:tt,
Expand Down Expand Up @@ -428,7 +428,7 @@ macro_rules! setup_tracked_fn {
let fn_ingredient = <$zalsa::function::IngredientImpl<$Configuration>>::new(
first_index,
memo_ingredient_indices,
$lru,
$eviction_capacity,
);
$zalsa::macro_if! {
if $needs_interner {
Expand Down Expand Up @@ -484,14 +484,14 @@ macro_rules! setup_tracked_fn {
}
}

/// Sets the lru capacity
/// Sets the eviction policy's capacity.
///
/// **WARNING:** Just like an ordinary write, this method triggers
/// cancellation. If you invoke it while a snapshot exists, it
/// will block until that snapshot is dropped -- if that snapshot
/// is owned by the current thread, this could trigger deadlock.
fn set_lru_capacity(db: &mut dyn $Db, value: usize) where for<'trivial_bounds> $Eviction: $zalsa::function::HasCapacity {
$Configuration::fn_ingredient_mut(db).set_capacity(value);
fn set_eviction_capacity(db: &mut dyn $Db, capacity: usize) where for<'trivial_bounds> $Eviction: $zalsa::function::HasCapacity {
$Configuration::fn_ingredient_mut(db).set_capacity(capacity);
}

$zalsa::macro_if! { $needs_interner =>
Expand Down
2 changes: 1 addition & 1 deletion components/salsa-macros/src/accumulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ impl AllowedOptions for Accumulator {
const CYCLE_FN: bool = false;
const CYCLE_INITIAL: bool = false;
const CYCLE_RESULT: bool = false;
const LRU: bool = false;
const EVICTION: bool = false;
const CONSTRUCTOR_NAME: bool = false;
const ID: bool = false;
const REVISIONS: bool = false;
Expand Down
2 changes: 1 addition & 1 deletion components/salsa-macros/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl AllowedOptions for InputStruct {

const CYCLE_RESULT: bool = false;

const LRU: bool = false;
const EVICTION: bool = false;

const CONSTRUCTOR_NAME: bool = true;

Expand Down
2 changes: 1 addition & 1 deletion components/salsa-macros/src/interned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl AllowedOptions for InternedStruct {

const CYCLE_RESULT: bool = false;

const LRU: bool = false;
const EVICTION: bool = false;

const CONSTRUCTOR_NAME: bool = true;

Expand Down
Loading