Skip to main content

subms_block_cache/features/
arc.rs

1//! Adaptive Replacement Cache (Megiddo + Modha, 2003).
2//!
3//! Four lists, total budget `c`:
4//!
5//!   T1 - recently-seen-once entries (LRU at the back).
6//!   T2 - recently-seen-more-than-once entries (LRU at the back).
7//!   B1 - ghost list of keys recently evicted from T1.
8//!   B2 - ghost list of keys recently evicted from T2.
9//!
10//! `|T1| + |T2| <= c`. `|T1| + |B1| <= c`, `|T2| + |B2| <= 2c`.
11//! The split between T1 and T2 is governed by `p` (target |T1| size),
12//! which adapts on ghost-list hits: a B1 hit grows `p` (recency
13//! signal); a B2 hit shrinks `p` (frequency signal). Scan-resistant
14//! because a one-shot scan only lifts entries into T1 and then evicts
15//! them to B1 without polluting T2.
16//!
17//! O(1) per access using a doubly-linked-list-by-index. We allocate a
18//! Node pool keyed by a u32 slot id, and the four "lists" are just
19//! head/tail pointers into that pool. Hashmap on `K -> (list_tag,
20//! slot_id)` for membership lookup.
21
22use std::collections::HashMap;
23use std::hash::Hash;
24
25/// Which list a slot currently lives on.
26#[derive(Clone, Copy, PartialEq, Eq, Debug)]
27enum List {
28    T1,
29    T2,
30    B1,
31    B2,
32}
33
34struct Node<K, V> {
35    key: K,
36    /// Resident-only. Ghost-list entries hold `None`.
37    value: Option<V>,
38    prev: u32,
39    next: u32,
40    list: List,
41}
42
43const NIL: u32 = u32::MAX;
44
45/// Adaptive replacement cache. `K: Hash + Eq + Clone`. `c` is the
46/// resident budget; the ghost lists may hold up to another `c` keys
47/// combined.
48pub struct ArcCache<K, V> {
49    c: usize,
50    p: usize,
51    nodes: Vec<Option<Node<K, V>>>,
52    free: Vec<u32>,
53    index: HashMap<K, u32>,
54    // Per-list head + tail + length.
55    t1_head: u32,
56    t1_tail: u32,
57    t1_len: usize,
58    t2_head: u32,
59    t2_tail: u32,
60    t2_len: usize,
61    b1_head: u32,
62    b1_tail: u32,
63    b1_len: usize,
64    b2_head: u32,
65    b2_tail: u32,
66    b2_len: usize,
67}
68
69impl<K: Hash + Eq + Clone, V> ArcCache<K, V> {
70    pub fn with_capacity(c: usize) -> Self {
71        let c = c.max(1);
72        Self {
73            c,
74            p: 0,
75            nodes: Vec::new(),
76            free: Vec::new(),
77            index: HashMap::new(),
78            t1_head: NIL,
79            t1_tail: NIL,
80            t1_len: 0,
81            t2_head: NIL,
82            t2_tail: NIL,
83            t2_len: 0,
84            b1_head: NIL,
85            b1_tail: NIL,
86            b1_len: 0,
87            b2_head: NIL,
88            b2_tail: NIL,
89            b2_len: 0,
90        }
91    }
92
93    pub fn capacity(&self) -> usize {
94        self.c
95    }
96    pub fn len(&self) -> usize {
97        self.t1_len + self.t2_len
98    }
99    pub fn is_empty(&self) -> bool {
100        self.len() == 0
101    }
102    pub fn p(&self) -> usize {
103        self.p
104    }
105    pub fn t1_len(&self) -> usize {
106        self.t1_len
107    }
108    pub fn t2_len(&self) -> usize {
109        self.t2_len
110    }
111    pub fn b1_len(&self) -> usize {
112        self.b1_len
113    }
114    pub fn b2_len(&self) -> usize {
115        self.b2_len
116    }
117
118    /// Get + promote. A T1 hit moves to T2 (now seen twice). A T2 hit
119    /// moves to T2's MRU end. Ghost-list keys are NOT counted as
120    /// resident hits and return None.
121    pub fn get(&mut self, key: &K) -> Option<&V> {
122        let id = *self.index.get(key)?;
123        match self.nodes[id as usize].as_ref().unwrap().list {
124            List::T1 => {
125                self.unlink(id);
126                self.t1_len -= 1;
127                self.nodes[id as usize].as_mut().unwrap().list = List::T2;
128                self.push_front_t2(id);
129                self.t2_len += 1;
130            }
131            List::T2 => {
132                self.unlink(id);
133                self.t2_len -= 1;
134                self.push_front_t2(id);
135                self.t2_len += 1;
136            }
137            // Ghost. Not a resident hit.
138            List::B1 | List::B2 => return None,
139        }
140        self.nodes[id as usize].as_ref().unwrap().value.as_ref()
141    }
142
143    /// Insert or update. Implements the ARC replacement policy.
144    pub fn put(&mut self, key: K, value: V) -> Option<(K, V)> {
145        if let Some(&id) = self.index.get(&key) {
146            match self.nodes[id as usize].as_ref().unwrap().list {
147                List::T1 => {
148                    // Promote to T2, update value.
149                    self.unlink(id);
150                    self.t1_len -= 1;
151                    let node = self.nodes[id as usize].as_mut().unwrap();
152                    node.value = Some(value);
153                    node.list = List::T2;
154                    self.push_front_t2(id);
155                    self.t2_len += 1;
156                    return None;
157                }
158                List::T2 => {
159                    self.unlink(id);
160                    let node = self.nodes[id as usize].as_mut().unwrap();
161                    node.value = Some(value);
162                    self.push_front_t2(id);
163                    return None;
164                }
165                List::B1 => {
166                    // Case II: B1 hit -> grow p, replace, move to T2.
167                    let delta = (self.b2_len.max(1) / self.b1_len.max(1)).max(1);
168                    self.p = (self.p + delta).min(self.c);
169                    let evicted = self.replace(false);
170                    self.unlink(id);
171                    self.b1_len -= 1;
172                    let node = self.nodes[id as usize].as_mut().unwrap();
173                    node.value = Some(value);
174                    node.list = List::T2;
175                    self.push_front_t2(id);
176                    self.t2_len += 1;
177                    return evicted;
178                }
179                List::B2 => {
180                    // Case III: B2 hit -> shrink p, replace, move to T2.
181                    let delta = (self.b1_len.max(1) / self.b2_len.max(1)).max(1);
182                    self.p = self.p.saturating_sub(delta);
183                    let evicted = self.replace(true);
184                    self.unlink(id);
185                    self.b2_len -= 1;
186                    let node = self.nodes[id as usize].as_mut().unwrap();
187                    node.value = Some(value);
188                    node.list = List::T2;
189                    self.push_front_t2(id);
190                    self.t2_len += 1;
191                    return evicted;
192                }
193            }
194        }
195
196        // Case IV: brand-new key. Insert into T1 (head). Maybe evict.
197        let l1 = self.t1_len + self.b1_len;
198        let l2 = self.t2_len + self.b2_len;
199        let mut evicted = None;
200        if l1 == self.c {
201            // |L1| == c
202            if self.t1_len < self.c {
203                // Drop LRU of B1; replace from resident.
204                if let Some(victim) = self.pop_lru_b1() {
205                    self.index.remove(&victim);
206                }
207                evicted = self.replace(false);
208            } else {
209                // |T1| == c, B1 empty - evict LRU of T1 outright (no ghost).
210                let id = self.t1_tail;
211                self.unlink(id);
212                self.t1_len -= 1;
213                let n = self.nodes[id as usize].take().unwrap();
214                self.index.remove(&n.key);
215                self.free.push(id);
216                evicted = Some((n.key, n.value.unwrap()));
217            }
218        } else if l1 + l2 >= self.c {
219            if l1 + l2 == 2 * self.c {
220                if let Some(victim) = self.pop_lru_b2() {
221                    self.index.remove(&victim);
222                }
223            }
224            evicted = self.replace(false);
225        }
226
227        let id = self.alloc(Node {
228            key: key.clone(),
229            value: Some(value),
230            prev: NIL,
231            next: NIL,
232            list: List::T1,
233        });
234        self.index.insert(key, id);
235        self.push_front_t1(id);
236        self.t1_len += 1;
237        evicted
238    }
239
240    /// ARC's REPLACE(L,x) routine. `b2_hit` says "we just had a B2 hit"
241    /// which biases eviction toward T1 even when |T1| == p.
242    fn replace(&mut self, b2_hit: bool) -> Option<(K, V)> {
243        let force_t1 = b2_hit && self.t1_len == self.p;
244        if self.t1_len > 0 && (self.t1_len > self.p || force_t1) {
245            // Evict LRU of T1 -> B1.
246            let id = self.t1_tail;
247            self.unlink(id);
248            self.t1_len -= 1;
249            let value = self.nodes[id as usize]
250                .as_mut()
251                .unwrap()
252                .value
253                .take()
254                .unwrap();
255            self.nodes[id as usize].as_mut().unwrap().list = List::B1;
256            self.push_front_b1(id);
257            self.b1_len += 1;
258            let key = self.nodes[id as usize].as_ref().unwrap().key.clone();
259            Some((key, value))
260        } else if self.t2_len > 0 {
261            let id = self.t2_tail;
262            self.unlink(id);
263            self.t2_len -= 1;
264            let value = self.nodes[id as usize]
265                .as_mut()
266                .unwrap()
267                .value
268                .take()
269                .unwrap();
270            self.nodes[id as usize].as_mut().unwrap().list = List::B2;
271            self.push_front_b2(id);
272            self.b2_len += 1;
273            let key = self.nodes[id as usize].as_ref().unwrap().key.clone();
274            Some((key, value))
275        } else {
276            None
277        }
278    }
279
280    fn alloc(&mut self, node: Node<K, V>) -> u32 {
281        if let Some(id) = self.free.pop() {
282            self.nodes[id as usize] = Some(node);
283            id
284        } else {
285            let id = self.nodes.len() as u32;
286            self.nodes.push(Some(node));
287            id
288        }
289    }
290
291    fn pop_lru_b1(&mut self) -> Option<K> {
292        if self.b1_tail == NIL {
293            return None;
294        }
295        let id = self.b1_tail;
296        self.unlink(id);
297        self.b1_len -= 1;
298        let n = self.nodes[id as usize].take().unwrap();
299        self.free.push(id);
300        Some(n.key)
301    }
302
303    fn pop_lru_b2(&mut self) -> Option<K> {
304        if self.b2_tail == NIL {
305            return None;
306        }
307        let id = self.b2_tail;
308        self.unlink(id);
309        self.b2_len -= 1;
310        let n = self.nodes[id as usize].take().unwrap();
311        self.free.push(id);
312        Some(n.key)
313    }
314
315    // Doubly-linked-list ops. Each list has its own head/tail; we use
316    // the node's `list` tag to know which head/tail to mutate.
317    fn unlink(&mut self, id: u32) {
318        let (prev, next, list) = {
319            let n = self.nodes[id as usize].as_ref().unwrap();
320            (n.prev, n.next, n.list)
321        };
322        if prev != NIL {
323            self.nodes[prev as usize].as_mut().unwrap().next = next;
324        }
325        if next != NIL {
326            self.nodes[next as usize].as_mut().unwrap().prev = prev;
327        }
328        let n = self.nodes[id as usize].as_mut().unwrap();
329        n.prev = NIL;
330        n.next = NIL;
331        match list {
332            List::T1 => {
333                if self.t1_head == id {
334                    self.t1_head = next;
335                }
336                if self.t1_tail == id {
337                    self.t1_tail = prev;
338                }
339            }
340            List::T2 => {
341                if self.t2_head == id {
342                    self.t2_head = next;
343                }
344                if self.t2_tail == id {
345                    self.t2_tail = prev;
346                }
347            }
348            List::B1 => {
349                if self.b1_head == id {
350                    self.b1_head = next;
351                }
352                if self.b1_tail == id {
353                    self.b1_tail = prev;
354                }
355            }
356            List::B2 => {
357                if self.b2_head == id {
358                    self.b2_head = next;
359                }
360                if self.b2_tail == id {
361                    self.b2_tail = prev;
362                }
363            }
364        }
365    }
366
367    fn push_front_t1(&mut self, id: u32) {
368        let old_head = self.t1_head;
369        self.nodes[id as usize].as_mut().unwrap().next = old_head;
370        self.nodes[id as usize].as_mut().unwrap().prev = NIL;
371        if old_head != NIL {
372            self.nodes[old_head as usize].as_mut().unwrap().prev = id;
373        }
374        self.t1_head = id;
375        if self.t1_tail == NIL {
376            self.t1_tail = id;
377        }
378    }
379    fn push_front_t2(&mut self, id: u32) {
380        let old_head = self.t2_head;
381        self.nodes[id as usize].as_mut().unwrap().next = old_head;
382        self.nodes[id as usize].as_mut().unwrap().prev = NIL;
383        if old_head != NIL {
384            self.nodes[old_head as usize].as_mut().unwrap().prev = id;
385        }
386        self.t2_head = id;
387        if self.t2_tail == NIL {
388            self.t2_tail = id;
389        }
390    }
391    fn push_front_b1(&mut self, id: u32) {
392        let old_head = self.b1_head;
393        self.nodes[id as usize].as_mut().unwrap().next = old_head;
394        self.nodes[id as usize].as_mut().unwrap().prev = NIL;
395        if old_head != NIL {
396            self.nodes[old_head as usize].as_mut().unwrap().prev = id;
397        }
398        self.b1_head = id;
399        if self.b1_tail == NIL {
400            self.b1_tail = id;
401        }
402    }
403    fn push_front_b2(&mut self, id: u32) {
404        let old_head = self.b2_head;
405        self.nodes[id as usize].as_mut().unwrap().next = old_head;
406        self.nodes[id as usize].as_mut().unwrap().prev = NIL;
407        if old_head != NIL {
408            self.nodes[old_head as usize].as_mut().unwrap().prev = id;
409        }
410        self.b2_head = id;
411        if self.b2_tail == NIL {
412            self.b2_tail = id;
413        }
414    }
415}
416
417#[cfg(test)]
418#[path = "arc_tests.rs"]
419mod tests;