Skip to main content

subms_block_cache/features/
weighted.rs

1//! Weighted block cache: per-entry byte size.
2//!
3//! The base cache assumes uniform entry size and counts capacity in
4//! slots. A weighted cache counts capacity in bytes. The caller
5//! supplies `size_of: fn(&V) -> usize` (or any closure). On `put`, the
6//! cache adds the entry's size to a running total; if the total
7//! exceeds `capacity_bytes` it evicts via clock-sweep (LRU-ish via
8//! second-chance) until the total fits again. Eviction may free more
9//! than the minimum needed for one put if the working set has many
10//! small entries.
11//!
12//! A single put whose value is itself larger than capacity is
13//! rejected: the cache cannot hold it, so we return the new
14//! key+value as the "evicted" pair without touching residents.
15
16use std::collections::HashMap;
17use std::hash::Hash;
18
19const NIL: u32 = u32::MAX;
20
21struct Slot<K, V> {
22    key: K,
23    value: V,
24    size: usize,
25    referenced: bool,
26}
27
28pub struct WeightedCache<K, V> {
29    capacity_bytes: usize,
30    used_bytes: usize,
31    slots: Vec<Option<Slot<K, V>>>,
32    index: HashMap<K, u32>,
33    hand: u32,
34    size_of: Box<dyn Fn(&V) -> usize>,
35    // Indices of slots that were vacated by eviction. `put` pops from
36    // here instead of scanning `slots` for a `None` hole, keeping the
37    // insert path O(1) under a steady eviction workload.
38    free_slots: Vec<u32>,
39}
40
41impl<K: Hash + Eq + Clone, V> WeightedCache<K, V> {
42    pub fn with_capacity_bytes<F>(capacity_bytes: usize, size_of: F) -> Self
43    where
44        F: Fn(&V) -> usize + 'static,
45    {
46        Self {
47            capacity_bytes: capacity_bytes.max(1),
48            used_bytes: 0,
49            slots: Vec::new(),
50            index: HashMap::new(),
51            hand: 0,
52            size_of: Box::new(size_of),
53            free_slots: Vec::new(),
54        }
55    }
56
57    pub fn capacity_bytes(&self) -> usize {
58        self.capacity_bytes
59    }
60    pub fn used_bytes(&self) -> usize {
61        self.used_bytes
62    }
63    pub fn len(&self) -> usize {
64        self.index.len()
65    }
66    pub fn is_empty(&self) -> bool {
67        self.index.is_empty()
68    }
69
70    pub fn get(&mut self, key: &K) -> Option<&V> {
71        let id = *self.index.get(key)?;
72        let s = self.slots[id as usize].as_mut().unwrap();
73        s.referenced = true;
74        Some(&self.slots[id as usize].as_ref().unwrap().value)
75    }
76
77    /// Insert or update. May evict multiple entries to fit the new one.
78    /// Returns a Vec of evicted (key, value) pairs in eviction order;
79    /// empty if nothing was evicted.
80    pub fn put(&mut self, key: K, value: V) -> Vec<(K, V)> {
81        let new_size = (self.size_of)(&value);
82
83        // Single-value-larger-than-capacity: reject outright.
84        if new_size > self.capacity_bytes {
85            return vec![(key, value)];
86        }
87
88        // Update path: replace in place; adjust used_bytes by delta.
89        if let Some(&id) = self.index.get(&key) {
90            let old_size = self.slots[id as usize].as_ref().unwrap().size;
91            let s = self.slots[id as usize].as_mut().unwrap();
92            s.value = value;
93            s.referenced = true;
94            s.size = new_size;
95            self.used_bytes = self.used_bytes + new_size - old_size;
96            // If the new value bloats us past capacity, evict others.
97            let mut evicted = Vec::new();
98            while self.used_bytes > self.capacity_bytes {
99                if let Some(ev) = self.sweep_evict_excluding(id) {
100                    evicted.push(ev);
101                } else {
102                    break;
103                }
104            }
105            return evicted;
106        }
107
108        // Insert path. Evict until there's room for `new_size`.
109        let mut evicted = Vec::new();
110        while self.used_bytes + new_size > self.capacity_bytes {
111            if let Some(ev) = self.sweep_evict_excluding(NIL) {
112                evicted.push(ev);
113            } else {
114                break;
115            }
116        }
117
118        // Reuse a vacated slot in O(1) via the free-slot stack; fall
119        // back to pushing a new slot only when none are free.
120        let new_id = if let Some(i) = self.free_slots.pop() {
121            self.slots[i as usize] = Some(Slot {
122                key: key.clone(),
123                value,
124                size: new_size,
125                referenced: true,
126            });
127            i
128        } else {
129            let id = self.slots.len() as u32;
130            self.slots.push(Some(Slot {
131                key: key.clone(),
132                value,
133                size: new_size,
134                referenced: true,
135            }));
136            id
137        };
138        self.index.insert(key, new_id);
139        self.used_bytes += new_size;
140        evicted
141    }
142
143    /// Sweep, evicting a non-referenced slot. Returns None if nothing
144    /// is resident. Sweep terminates because every populated,
145    /// non-skipped slot has its ref bit cleared on first visit and
146    /// is evictable on second visit. Total visits bounded by 2n + 1.
147    fn sweep_evict_excluding(&mut self, skip: u32) -> Option<(K, V)> {
148        if self.index.is_empty() {
149            return None;
150        }
151        let n = self.slots.len();
152        if n == 0 {
153            return None;
154        }
155        for _ in 0..(2 * n + 1) {
156            let i = (self.hand as usize) % n;
157            self.hand = ((self.hand as usize + 1) % n.max(1)) as u32;
158            let want_evict = {
159                let s = match self.slots[i].as_mut() {
160                    Some(s) => s,
161                    None => continue,
162                };
163                if (i as u32) == skip {
164                    false
165                } else if s.referenced {
166                    s.referenced = false;
167                    false
168                } else {
169                    true
170                }
171            };
172            if want_evict {
173                let s = self.slots[i].take().unwrap();
174                self.index.remove(&s.key);
175                self.used_bytes -= s.size;
176                self.free_slots.push(i as u32);
177                return Some((s.key, s.value));
178            }
179        }
180        None
181    }
182}
183
184#[cfg(test)]
185#[path = "weighted_tests.rs"]
186mod tests;