Skip to main content

subms_block_cache/features/
tinylfu.rs

1//! W-TinyLFU admission policy (Einziger, Friedman + Manes, ACM TOS 13(4),
2//! 2017).
3//!
4//! Cache is split into:
5//!
6//!   Window (1% of capacity) - small LRU. New keys enter here.
7//!   Main protected (80% of main) - LRU; the popular working set.
8//!   Main probation  (20% of main) - LRU; recent demotees + recent admits.
9//!
10//! When the Window evicts an entry, that entry becomes a CANDIDATE.
11//! The candidate fights for admission against Main probation's LRU
12//! VICTIM. We consult a count-min sketch (CMS) of recent access
13//! frequencies and admit the candidate iff its frequency >= victim's
14//! frequency.
15//!
16//! Doorkeeper: a small bloom filter that filters out "seen exactly
17//! once" candidates before they ever touch the CMS. Reduces sketch
18//! pressure.
19//!
20//! CMS uses a periodic "aging" pass: every `sample_size = 10*c`
21//! accesses, all counters halve. This bounds the influence of stale
22//! popularity bursts.
23//!
24//! The CMS implementation lives in this file rather than depending on
25//! the sibling `subms-count-min-sketch` recipe. Reason: the cache is
26//! a leaf in the dep graph; pulling in a sibling recipe would create
27//! a cyclic-refresh risk on per-recipe releases and force consumers to
28//! pull two crates for one feature. The local CMS is 4 rows of 32-bit
29//! counters keyed by FNV-1a + linear probing, sized to 4 * c counters.
30
31use std::collections::HashMap;
32use std::hash::{Hash, Hasher};
33
34const FNV_OFFSET: u64 = 0xcbf29ce484222325;
35const FNV_PRIME: u64 = 0x100000001b3;
36
37const CMS_ROWS: usize = 4;
38const CMS_COUNTER_MAX: u32 = 15;
39
40const NIL: u32 = u32::MAX;
41
42struct Node<K, V> {
43    key: K,
44    value: V,
45    prev: u32,
46    next: u32,
47    /// Window, Probation, or Protected.
48    list: Segment,
49}
50
51#[derive(Copy, Clone, PartialEq, Eq)]
52enum Segment {
53    Window,
54    Probation,
55    Protected,
56}
57
58/// Count-min sketch with `CMS_ROWS` rows and `cols` counters per row.
59/// Each counter is 4 bits (packed into u8). On insert we increment the
60/// counter in every row; on query we return the min across rows.
61struct Cms {
62    cols: usize,
63    rows: Vec<Vec<u8>>,
64    additions: u64,
65    sample_size: u64,
66}
67
68impl Cms {
69    fn new(cells_per_row: usize, sample_size: u64) -> Self {
70        let cols = cells_per_row.max(8).next_power_of_two();
71        Self {
72            cols,
73            rows: (0..CMS_ROWS).map(|_| vec![0u8; cols.div_ceil(2)]).collect(),
74            additions: 0,
75            sample_size,
76        }
77    }
78
79    fn idx(&self, row: usize, h: u64) -> usize {
80        let seeded = h.wrapping_mul(SEEDS[row]).wrapping_add(SEEDS[row] >> 17);
81        (seeded as usize) & (self.cols - 1)
82    }
83
84    fn read(&self, row: usize, idx: usize) -> u8 {
85        let byte = self.rows[row][idx / 2];
86        if idx & 1 == 0 {
87            byte & 0x0f
88        } else {
89            (byte >> 4) & 0x0f
90        }
91    }
92    fn write(&mut self, row: usize, idx: usize, v: u8) {
93        let i = idx / 2;
94        let v = v & 0x0f;
95        if idx & 1 == 0 {
96            self.rows[row][i] = (self.rows[row][i] & 0xf0) | v;
97        } else {
98            self.rows[row][i] = (self.rows[row][i] & 0x0f) | (v << 4);
99        }
100    }
101
102    fn increment(&mut self, hash: u64) {
103        let mut added = false;
104        for r in 0..CMS_ROWS {
105            let idx = self.idx(r, hash);
106            let cur = self.read(r, idx) as u32;
107            if cur < CMS_COUNTER_MAX {
108                self.write(r, idx, (cur + 1) as u8);
109                added = true;
110            }
111        }
112        if added {
113            self.additions += 1;
114            if self.additions >= self.sample_size {
115                self.reset();
116            }
117        }
118    }
119
120    fn estimate(&self, hash: u64) -> u32 {
121        let mut m = u32::MAX;
122        for r in 0..CMS_ROWS {
123            let idx = self.idx(r, hash);
124            let v = self.read(r, idx) as u32;
125            if v < m {
126                m = v;
127            }
128        }
129        m
130    }
131
132    /// Halve every counter. Standard W-TinyLFU aging.
133    fn reset(&mut self) {
134        for r in 0..CMS_ROWS {
135            for byte in self.rows[r].iter_mut() {
136                let lo = (*byte & 0x0f) >> 1;
137                let hi = ((*byte >> 4) & 0x0f) >> 1;
138                *byte = (hi << 4) | lo;
139            }
140        }
141        self.additions /= 2;
142    }
143}
144
145const SEEDS: [u64; CMS_ROWS] = [
146    0x9e3779b97f4a7c15,
147    0xbf58476d1ce4e5b9,
148    0x94d049bb133111eb,
149    0x2545f4914f6cdd1d,
150];
151
152/// Doorkeeper: tiny bloom filter; toggles "have we seen this key at
153/// least once recently". Catches the singleton scan case before it
154/// pollutes the CMS counters.
155struct Doorkeeper {
156    bits: Vec<u64>,
157    mask: u64,
158}
159
160impl Doorkeeper {
161    fn new(bits: usize) -> Self {
162        let words = bits.max(64).next_power_of_two().div_ceil(64);
163        Self {
164            bits: vec![0u64; words],
165            mask: ((words * 64) as u64) - 1,
166        }
167    }
168    fn idx(&self, hash: u64, salt: u64) -> usize {
169        ((hash.wrapping_mul(salt)) & self.mask) as usize
170    }
171    /// Returns true if the key was already in the doorkeeper.
172    fn check_or_add(&mut self, hash: u64) -> bool {
173        let positions = [self.idx(hash, SEEDS[0]), self.idx(hash, SEEDS[1])];
174        let mut all_set = true;
175        for &p in &positions {
176            let word = p >> 6;
177            let bit = 1u64 << (p & 63);
178            if self.bits[word] & bit == 0 {
179                all_set = false;
180                self.bits[word] |= bit;
181            }
182        }
183        all_set
184    }
185    fn clear(&mut self) {
186        for w in self.bits.iter_mut() {
187            *w = 0;
188        }
189    }
190}
191
192/// W-TinyLFU cache.
193pub struct TinyLfuCache<K, V> {
194    c: usize,
195    window_cap: usize,
196    protected_cap: usize,
197    probation_cap: usize,
198    nodes: Vec<Option<Node<K, V>>>,
199    free: Vec<u32>,
200    index: HashMap<K, u32>,
201    window_head: u32,
202    window_tail: u32,
203    window_len: usize,
204    protected_head: u32,
205    protected_tail: u32,
206    protected_len: usize,
207    probation_head: u32,
208    probation_tail: u32,
209    probation_len: usize,
210    cms: Cms,
211    doorkeeper: Doorkeeper,
212    admissions: u64,
213    rejections: u64,
214}
215
216impl<K: Hash + Eq + Clone, V> TinyLfuCache<K, V> {
217    pub fn with_capacity(c: usize) -> Self {
218        let c = c.max(4);
219        // 1% window (floor of 1).
220        let window_cap = (c / 100).max(1);
221        let main = c - window_cap;
222        let protected_cap = (main * 4 / 5).max(1);
223        let probation_cap = (main - protected_cap).max(1);
224        let cms_cells = (c * 4).max(64);
225        let sample_size = (10 * c as u64).max(64);
226        Self {
227            c,
228            window_cap,
229            protected_cap,
230            probation_cap,
231            nodes: Vec::new(),
232            free: Vec::new(),
233            index: HashMap::new(),
234            window_head: NIL,
235            window_tail: NIL,
236            window_len: 0,
237            protected_head: NIL,
238            protected_tail: NIL,
239            protected_len: 0,
240            probation_head: NIL,
241            probation_tail: NIL,
242            probation_len: 0,
243            cms: Cms::new(cms_cells, sample_size),
244            doorkeeper: Doorkeeper::new(c * 8),
245            admissions: 0,
246            rejections: 0,
247        }
248    }
249
250    pub fn capacity(&self) -> usize {
251        self.c
252    }
253    pub fn len(&self) -> usize {
254        self.window_len + self.protected_len + self.probation_len
255    }
256    pub fn is_empty(&self) -> bool {
257        self.len() == 0
258    }
259    pub fn admissions(&self) -> u64 {
260        self.admissions
261    }
262    pub fn rejections(&self) -> u64 {
263        self.rejections
264    }
265    pub fn window_len(&self) -> usize {
266        self.window_len
267    }
268    pub fn protected_len(&self) -> usize {
269        self.protected_len
270    }
271    pub fn probation_len(&self) -> usize {
272        self.probation_len
273    }
274
275    fn record_access(&mut self, hash: u64) {
276        // If already past the doorkeeper, count in CMS proper; else
277        // just record in the doorkeeper.
278        if self.doorkeeper.check_or_add(hash) {
279            self.cms.increment(hash);
280            // Aging side-effect: when CMS resets (halves), also clear
281            // doorkeeper to avoid permanent saturation.
282            if self.cms.additions == 0 {
283                self.doorkeeper.clear();
284            }
285        }
286    }
287
288    pub fn get(&mut self, key: &K) -> Option<&V> {
289        let h = hash_one(key);
290        self.record_access(h);
291        let id = *self.index.get(key)?;
292        let segment = self.nodes[id as usize].as_ref().unwrap().list;
293        match segment {
294            Segment::Window => {
295                self.unlink(id);
296                self.push_front_window(id);
297            }
298            Segment::Probation => {
299                // Promote into Protected.
300                self.unlink(id);
301                self.probation_len -= 1;
302                if self.protected_len >= self.protected_cap {
303                    // Demote LRU of Protected back to Probation.
304                    let demote = self.protected_tail;
305                    self.unlink(demote);
306                    self.protected_len -= 1;
307                    self.nodes[demote as usize].as_mut().unwrap().list = Segment::Probation;
308                    self.push_front_probation(demote);
309                    self.probation_len += 1;
310                }
311                self.nodes[id as usize].as_mut().unwrap().list = Segment::Protected;
312                self.push_front_protected(id);
313                self.protected_len += 1;
314            }
315            Segment::Protected => {
316                self.unlink(id);
317                self.push_front_protected(id);
318            }
319        }
320        self.nodes[id as usize].as_ref().map(|n| &n.value)
321    }
322
323    /// Insert or update. Returns the rejected/evicted key+value if any.
324    pub fn put(&mut self, key: K, value: V) -> Option<(K, V)> {
325        let h = hash_one(&key);
326        self.record_access(h);
327
328        if let Some(&id) = self.index.get(&key) {
329            let n = self.nodes[id as usize].as_mut().unwrap();
330            n.value = value;
331            let seg = n.list;
332            self.unlink(id);
333            match seg {
334                Segment::Window => self.push_front_window(id),
335                Segment::Probation => self.push_front_probation(id),
336                Segment::Protected => self.push_front_protected(id),
337            }
338            return None;
339        }
340
341        // Try to fit into Window first.
342        if self.window_len < self.window_cap {
343            let id = self.alloc(Node {
344                key: key.clone(),
345                value,
346                prev: NIL,
347                next: NIL,
348                list: Segment::Window,
349            });
350            self.index.insert(key, id);
351            self.push_front_window(id);
352            self.window_len += 1;
353            return None;
354        }
355
356        // Window full: eject window-LRU as the candidate; either move
357        // to Probation or fight Probation-LRU for admission.
358        let candidate_id = self.window_tail;
359        self.unlink(candidate_id);
360        self.window_len -= 1;
361
362        // The new key takes the window head slot.
363        let new_id = self.alloc(Node {
364            key: key.clone(),
365            value,
366            prev: NIL,
367            next: NIL,
368            list: Segment::Window,
369        });
370        self.index.insert(key, new_id);
371        self.push_front_window(new_id);
372        self.window_len += 1;
373
374        // Where does the candidate go?
375        if self.probation_len < self.probation_cap {
376            // Probation has room - move candidate in, no fight.
377            self.nodes[candidate_id as usize].as_mut().unwrap().list = Segment::Probation;
378            self.push_front_probation(candidate_id);
379            self.probation_len += 1;
380            self.admissions += 1;
381            return None;
382        }
383
384        // Probation full - admission fight.
385        let victim_id = self.probation_tail;
386        let cand_hash = hash_one(&self.nodes[candidate_id as usize].as_ref().unwrap().key);
387        let vic_hash = hash_one(&self.nodes[victim_id as usize].as_ref().unwrap().key);
388        let cand_freq = self.cms.estimate(cand_hash);
389        let vic_freq = self.cms.estimate(vic_hash);
390
391        if cand_freq >= vic_freq {
392            // Admit. Evict victim.
393            self.unlink(victim_id);
394            self.probation_len -= 1;
395            let v = self.nodes[victim_id as usize].take().unwrap();
396            self.index.remove(&v.key);
397            self.free.push(victim_id);
398
399            self.nodes[candidate_id as usize].as_mut().unwrap().list = Segment::Probation;
400            self.push_front_probation(candidate_id);
401            self.probation_len += 1;
402            self.admissions += 1;
403            Some((v.key, v.value))
404        } else {
405            // Reject candidate; drop it.
406            let c = self.nodes[candidate_id as usize].take().unwrap();
407            self.index.remove(&c.key);
408            self.free.push(candidate_id);
409            self.rejections += 1;
410            Some((c.key, c.value))
411        }
412    }
413
414    fn alloc(&mut self, n: Node<K, V>) -> u32 {
415        if let Some(id) = self.free.pop() {
416            self.nodes[id as usize] = Some(n);
417            id
418        } else {
419            let id = self.nodes.len() as u32;
420            self.nodes.push(Some(n));
421            id
422        }
423    }
424
425    fn unlink(&mut self, id: u32) {
426        let (prev, next, seg) = {
427            let n = self.nodes[id as usize].as_ref().unwrap();
428            (n.prev, n.next, n.list)
429        };
430        if prev != NIL {
431            self.nodes[prev as usize].as_mut().unwrap().next = next;
432        }
433        if next != NIL {
434            self.nodes[next as usize].as_mut().unwrap().prev = prev;
435        }
436        let n = self.nodes[id as usize].as_mut().unwrap();
437        n.prev = NIL;
438        n.next = NIL;
439        match seg {
440            Segment::Window => {
441                if self.window_head == id {
442                    self.window_head = next;
443                }
444                if self.window_tail == id {
445                    self.window_tail = prev;
446                }
447            }
448            Segment::Probation => {
449                if self.probation_head == id {
450                    self.probation_head = next;
451                }
452                if self.probation_tail == id {
453                    self.probation_tail = prev;
454                }
455            }
456            Segment::Protected => {
457                if self.protected_head == id {
458                    self.protected_head = next;
459                }
460                if self.protected_tail == id {
461                    self.protected_tail = prev;
462                }
463            }
464        }
465    }
466
467    fn push_front_window(&mut self, id: u32) {
468        let old = self.window_head;
469        let n = self.nodes[id as usize].as_mut().unwrap();
470        n.next = old;
471        n.prev = NIL;
472        if old != NIL {
473            self.nodes[old as usize].as_mut().unwrap().prev = id;
474        }
475        self.window_head = id;
476        if self.window_tail == NIL {
477            self.window_tail = id;
478        }
479    }
480    fn push_front_protected(&mut self, id: u32) {
481        let old = self.protected_head;
482        let n = self.nodes[id as usize].as_mut().unwrap();
483        n.next = old;
484        n.prev = NIL;
485        if old != NIL {
486            self.nodes[old as usize].as_mut().unwrap().prev = id;
487        }
488        self.protected_head = id;
489        if self.protected_tail == NIL {
490            self.protected_tail = id;
491        }
492    }
493    fn push_front_probation(&mut self, id: u32) {
494        let old = self.probation_head;
495        let n = self.nodes[id as usize].as_mut().unwrap();
496        n.next = old;
497        n.prev = NIL;
498        if old != NIL {
499            self.nodes[old as usize].as_mut().unwrap().prev = id;
500        }
501        self.probation_head = id;
502        if self.probation_tail == NIL {
503            self.probation_tail = id;
504        }
505    }
506}
507
508fn hash_one<K: Hash>(k: &K) -> u64 {
509    let mut h = FnvHasher::new();
510    k.hash(&mut h);
511    h.finish()
512}
513
514struct FnvHasher(u64);
515impl FnvHasher {
516    fn new() -> Self {
517        FnvHasher(FNV_OFFSET)
518    }
519}
520impl Hasher for FnvHasher {
521    fn finish(&self) -> u64 {
522        self.0
523    }
524    fn write(&mut self, bytes: &[u8]) {
525        for &b in bytes {
526            self.0 ^= b as u64;
527            self.0 = self.0.wrapping_mul(FNV_PRIME);
528        }
529    }
530}
531
532#[cfg(test)]
533#[path = "tinylfu_tests.rs"]
534mod tests;