diff --git a/Cargo.lock b/Cargo.lock index 2c229b1..d4e79b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,9 +77,9 @@ checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arbitrary" @@ -351,9 +351,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] diff --git a/benches/workloads.rs b/benches/workloads.rs index faae7da..904e9e3 100644 --- a/benches/workloads.rs +++ b/benches/workloads.rs @@ -99,7 +99,13 @@ fn bench_hit_rates(c: &mut Criterion) { &mut generator, OPS, &mut op_model, - |k| Arc::clone(&value_pool[k as usize]), + |k| { + Arc::clone( + value_pool + .get(k as usize) + .expect("generator produced key outside configured UNIVERSE"), + ) + }, ); total += start.elapsed(); } @@ -214,7 +220,16 @@ fn bench_comprehensive(c: &mut Criterion) { policy_id, &mut cache, cfg, - |key| Arc::clone(&value_pool[key as usize]), + |key| { + let idx = key as usize; + Arc::clone(value_pool.get(idx).unwrap_or_else(|| { + panic!( + "workload key {} out of range for value_pool (len {}, expected < UNIVERSE)", + key, + value_pool.len() + ) + })) + }, ); total += start.elapsed(); } diff --git a/src/ds/intrusive_list.rs b/src/ds/intrusive_list.rs index a04cdc1..6e79008 100644 --- a/src/ds/intrusive_list.rs +++ b/src/ds/intrusive_list.rs @@ -871,11 +871,10 @@ impl IntrusiveList { #[inline] fn attach_front(&mut self, id: SlotId) -> Option<()> { let old_head = self.head; - if let Some(node) = self.arena.get_mut(id) { + { + let node = self.arena.get_mut(id)?; node.prev = None; node.next = old_head; - } else { - return None; } if let Some(old_head) = old_head { if let Some(head_node) = self.arena.get_mut(old_head) { @@ -891,11 +890,10 @@ impl IntrusiveList { #[inline] fn attach_back(&mut self, id: SlotId) -> Option<()> { let old_tail = self.tail; - if let Some(node) = self.arena.get_mut(id) { + { + let node = self.arena.get_mut(id)?; node.next = None; node.prev = old_tail; - } else { - return None; } if let Some(old_tail) = old_tail { if let Some(tail_node) = self.arena.get_mut(old_tail) { diff --git a/src/policy/s3_fifo.rs b/src/policy/s3_fifo.rs index 316c4f6..666f690 100644 --- a/src/policy/s3_fifo.rs +++ b/src/policy/s3_fifo.rs @@ -633,6 +633,8 @@ where /// hit/miss counters. #[cfg(feature = "concurrency")] #[inline] + // `try_update` is the nightly name, but `fetch_update` is required by our Rust 1.85 MSRV. + #[allow(deprecated)] pub(crate) fn get_shared(&self, key: &K) -> Option<&V> { let &id = self.map.get(key)?; let node = self.arena.get(id).expect("map/arena out of sync"); diff --git a/tests/abstract_models/reference/lfu.rs b/tests/abstract_models/reference/lfu.rs index c6c77ff..527ae69 100644 --- a/tests/abstract_models/reference/lfu.rs +++ b/tests/abstract_models/reference/lfu.rs @@ -3,7 +3,7 @@ //! **Source:** [`docs/testing/specs/policies/exact/lfu.md`](../../../docs/testing/specs/policies/exact/lfu.md) ยท //! [matrix.md](../../../docs/testing/specs/matrix.md) //! **Tier:** reference (spec-first oracle). -//! **Formulation:** `HashMap` + append-only `first_seen` log for FIFO tie-break; +//! **Formulation:** `HashMap` + append-only bucket-arrival log for FIFO tie-break; //! independent of [`FrequencyBuckets`](cachekit::ds::FrequencyBuckets). use std::collections::{HashMap, HashSet, VecDeque}; @@ -14,7 +14,7 @@ use crate::abstract_models::{HitMiss, ModelStep, Op, OracleExpectation, PolicyMo #[derive(Debug, Clone)] pub struct NaiveLfuModel { freq: HashMap, - first_seen: VecDeque, + bucket_arrivals: VecDeque, capacity: usize, } @@ -25,7 +25,7 @@ where pub fn new(capacity: usize) -> Self { Self { freq: HashMap::new(), - first_seen: VecDeque::new(), + bucket_arrivals: VecDeque::new(), capacity, } } @@ -42,9 +42,9 @@ where self.freq.values().copied().min() } - /// Last append index for `key` in the tie-break log (re-inserts append again). - fn last_seen_index(&self, key: &K) -> usize { - self.first_seen + /// Last bucket-arrival index for `key` (touches and re-inserts append again). + fn bucket_arrival_index(&self, key: &K) -> usize { + self.bucket_arrivals .iter() .rposition(|k| k == key) .unwrap_or(usize::MAX) @@ -56,7 +56,7 @@ where .iter() .filter(|(_, f)| **f == min) .map(|(k, _)| k) - .min_by_key(|k| self.last_seen_index(k)) + .min_by_key(|k| self.bucket_arrival_index(k)) .cloned() } @@ -69,6 +69,7 @@ where fn touch(&mut self, key: &K) -> bool { if let Some(f) = self.freq.get_mut(key) { *f = f.saturating_add(1); + self.bucket_arrivals.push_back(key.clone()); true } else { false @@ -84,7 +85,7 @@ where evicted = self.evict_victim(); } self.freq.insert(key.clone(), 1); - self.first_seen.push_back(key); + self.bucket_arrivals.push_back(key); evicted } diff --git a/tests/policy_semantics/lfu_tests.rs b/tests/policy_semantics/lfu_tests.rs index a86d82a..625b935 100644 --- a/tests/policy_semantics/lfu_tests.rs +++ b/tests/policy_semantics/lfu_tests.rs @@ -95,6 +95,20 @@ fn smoke_lfu_naive_agreement() { assert_models_agree(&mut naive, &mut current, &ops); } +#[test] +fn lfu_naive_agreement_uses_bucket_arrival_for_ties() { + let ops = [ + Op::Insert(1), + Op::Insert(2), + Op::Touch(2), + Op::Touch(1), + Op::EvictOne, + ]; + let mut naive = NaiveLfuModel::new(2); + let mut current = LfuModel::new(2); + assert_models_agree(&mut naive, &mut current, &ops); +} + #[test] fn smoke_lfu() { let ops = [