Skip to content

Commit de0517e

Browse files
authored
Refactor ClockRing insert logic to optimize slot usage and prevent unnecessary evictions (#26)
- Enhanced the `insert` method to utilize empty slots when available, ensuring that entries are not evicted when the cache has free space. - Implemented a two-pass CLOCK sweep for eviction, maintaining reference bits for entries to improve eviction accuracy. - Added regression tests to verify that inserts do not evict existing entries when there are available slots, ensuring correct behavior under various scenarios. - Updated documentation and tests to reflect these changes, improving clarity and usability of the `ClockRing` API.
1 parent 9310b81 commit de0517e

2 files changed

Lines changed: 124 additions & 39 deletions

File tree

src/ds/clock_ring.rs

Lines changed: 114 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,40 +1463,56 @@ where
14631463
return None;
14641464
}
14651465

1466-
loop {
1467-
let idx = self.hand;
1468-
if self.slots[idx].is_some() {
1469-
if self.referenced[idx] {
1466+
// Not full — scan for an empty slot without disturbing ref bits.
1467+
if self.len < self.capacity() {
1468+
let cap = self.capacity();
1469+
for offset in 0..cap {
1470+
let idx = (self.hand + offset) % cap;
1471+
if self.slots[idx].is_none() {
1472+
let entry_key = key.clone();
1473+
self.slots[idx] = Some(Entry {
1474+
key: entry_key,
1475+
value,
1476+
});
14701477
self.referenced[idx] = false;
1471-
self.advance_hand();
1472-
continue;
1478+
self.index.insert(key, idx);
1479+
self.len += 1;
1480+
self.hand = (idx + 1) % cap;
1481+
return None;
14731482
}
1483+
}
1484+
debug_assert!(false, "len < capacity but no empty slot found");
1485+
}
14741486

1475-
let evicted = self.slots[idx].take().expect("occupied slot missing");
1476-
self.index.remove(&evicted.key);
1477-
1478-
let entry_key = key.clone();
1479-
self.slots[idx] = Some(Entry {
1480-
key: entry_key,
1481-
value,
1482-
});
1487+
// Full — CLOCK sweep to find and evict a victim.
1488+
// One pass clears all ref bits; the second finds an unreferenced slot.
1489+
let cap = self.capacity();
1490+
for _ in 0..(2 * cap) {
1491+
let idx = self.hand;
1492+
if self.referenced[idx] {
14831493
self.referenced[idx] = false;
1484-
self.index.insert(key, idx);
14851494
self.advance_hand();
1486-
return Some((evicted.key, evicted.value));
1495+
continue;
14871496
}
14881497

1498+
let evicted = self.slots[idx].take().expect("occupied slot missing");
1499+
self.index.remove(&evicted.key);
1500+
14891501
let entry_key = key.clone();
14901502
self.slots[idx] = Some(Entry {
14911503
key: entry_key,
14921504
value,
14931505
});
14941506
self.referenced[idx] = false;
14951507
self.index.insert(key, idx);
1496-
self.len += 1;
14971508
self.advance_hand();
1498-
return None;
1509+
return Some((evicted.key, evicted.value));
14991510
}
1511+
debug_assert!(
1512+
false,
1513+
"insert sweep exceeded 2*capacity without finding victim"
1514+
);
1515+
None
15001516
}
15011517

15021518
/// Peeks the next eviction candidate without modifying state.
@@ -2012,13 +2028,10 @@ mod tests {
20122028
assert!(ring.peek(&"b").is_none());
20132029

20142030
let evicted = ring.insert("d", 4);
2031+
assert_eq!(evicted, None, "ring has free slot, must not evict");
20152032
assert!(ring.contains(&"d"));
20162033
assert!(!ring.contains(&"b"));
2017-
if evicted.is_some() {
2018-
assert_eq!(ring.len(), 2);
2019-
} else {
2020-
assert_eq!(ring.len(), 3);
2021-
}
2034+
assert_eq!(ring.len(), 3);
20222035
}
20232036

20242037
#[test]
@@ -2368,6 +2381,84 @@ mod tests {
23682381
let pairs: Vec<_> = cache.into_inner().into_iter().collect();
23692382
assert_eq!(pairs.len(), 2);
23702383
}
2384+
2385+
// -----------------------------------------------------------------------
2386+
// Regression: insert must not evict when empty slots exist (len < cap)
2387+
// -----------------------------------------------------------------------
2388+
2389+
#[test]
2390+
fn insert_uses_empty_slot_after_remove_no_eviction() {
2391+
let mut ring = ClockRing::new(3);
2392+
ring.insert("a", 1); // slot 0, hand -> 1
2393+
ring.insert("b", 2); // slot 1, hand -> 2
2394+
ring.insert("c", 3); // slot 2, hand -> 0
2395+
2396+
ring.remove(&"b"); // slot 1 now empty, len = 2
2397+
assert_eq!(ring.len(), 2);
2398+
2399+
// Ring has room (len=2 < cap=3). Insert must use the empty slot,
2400+
// not evict a live entry.
2401+
let evicted = ring.insert("d", 4);
2402+
assert_eq!(evicted, None, "must not evict when ring has free slots");
2403+
assert_eq!(ring.len(), 3);
2404+
assert!(ring.contains(&"a"), "\"a\" should still be present");
2405+
assert!(ring.contains(&"c"), "\"c\" should still be present");
2406+
assert!(ring.contains(&"d"), "\"d\" should have been inserted");
2407+
ring.debug_validate_invariants();
2408+
}
2409+
2410+
#[test]
2411+
fn insert_uses_empty_slot_after_pop_victim_no_eviction() {
2412+
let mut ring = ClockRing::new(3);
2413+
ring.insert("a", 1);
2414+
ring.insert("b", 2);
2415+
ring.insert("c", 3);
2416+
2417+
let victim = ring.pop_victim();
2418+
assert!(victim.is_some());
2419+
assert_eq!(ring.len(), 2);
2420+
2421+
// After pop_victim freed a slot, insert must not evict again.
2422+
let evicted = ring.insert("d", 4);
2423+
assert_eq!(evicted, None, "must not evict when ring has free slots");
2424+
assert_eq!(ring.len(), 3);
2425+
assert!(ring.contains(&"d"));
2426+
ring.debug_validate_invariants();
2427+
}
2428+
2429+
#[test]
2430+
fn insert_no_eviction_preserves_ref_bits() {
2431+
let mut ring = ClockRing::new(3);
2432+
ring.insert("a", 1); // slot 0, hand -> 1
2433+
ring.insert("b", 2); // slot 1, hand -> 2
2434+
ring.insert("c", 3); // slot 2, hand -> 0
2435+
2436+
ring.touch(&"a"); // ref[0] = true
2437+
2438+
ring.remove(&"c"); // slot 2 = None, hand stays 0, len = 2
2439+
2440+
// Non-full insert: must find the empty slot without clearing
2441+
// "a"'s reference bit.
2442+
let evicted = ring.insert("d", 4);
2443+
assert_eq!(evicted, None, "must not evict when ring has free slots");
2444+
assert_eq!(ring.len(), 3);
2445+
2446+
// Now full. Insert triggers eviction. If "a"'s ref bit was
2447+
// preserved, "a" survives; victim is "b" or "d".
2448+
let evicted = ring.insert("e", 5);
2449+
assert!(evicted.is_some());
2450+
let (evicted_key, _) = evicted.unwrap();
2451+
assert!(
2452+
evicted_key == "b" || evicted_key == "d",
2453+
"expected unreferenced victim, got {:?}",
2454+
evicted_key
2455+
);
2456+
assert!(
2457+
ring.contains(&"a"),
2458+
"\"a\" should survive: ref bit preserved"
2459+
);
2460+
ring.debug_validate_invariants();
2461+
}
23712462
}
23722463

23732464
#[cfg(test)]

src/policy/clock.rs

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -304,27 +304,21 @@ where
304304
}
305305
}
306306

307-
// SAFETY: ClockCache can be sent between threads if K and V are Send.
308-
unsafe impl<K, V> Send for ClockCache<K, V>
309-
where
310-
K: Clone + Eq + Hash + Send,
311-
V: Send,
312-
{
313-
}
314-
315-
// SAFETY: ClockCache can be shared between threads if K and V are Sync.
316-
unsafe impl<K, V> Sync for ClockCache<K, V>
317-
where
318-
K: Clone + Eq + Hash + Sync,
319-
V: Sync,
320-
{
321-
}
322-
323307
#[cfg(test)]
324308
mod tests {
325309
use super::*;
326310
use crate::traits::MutableCache;
327311

312+
#[allow(dead_code)]
313+
const _: () = {
314+
fn assert_send<T: Send>() {}
315+
fn assert_sync<T: Sync>() {}
316+
fn check() {
317+
assert_send::<ClockCache<String, i32>>();
318+
assert_sync::<ClockCache<String, i32>>();
319+
}
320+
};
321+
328322
mod basic_operations {
329323
use super::*;
330324

0 commit comments

Comments
 (0)