Skip to main content

cranpose_core/collections/
bounded_lru.rs

1use std::{
2    borrow::Borrow,
3    hash::{BuildHasher, Hash},
4    num::NonZeroUsize,
5};
6
7use hashbrown::HashTable;
8
9use crate::collections::map::RandomState;
10
11struct CacheSlot<K, V> {
12    key: K,
13    value: V,
14    newer: Option<u32>,
15    older: Option<u32>,
16}
17
18/// A small bounded LRU cache for hot-path caches.
19///
20/// Hits update recency in place, so the common path is a single hash lookup.
21/// Eviction unlinks the oldest entry, which costs the same whether the cache
22/// holds ten entries or ten thousand.
23///
24/// The recency order is a linked list rather than a timestamp per entry
25/// because a timestamp makes eviction a scan for the minimum. These caches are
26/// large -- thousands of glyph masks -- and the workloads that need them most
27/// are the ones that miss steadily: text whose size animates re-rasterises
28/// every glyph of every frame, and every one of those inserts was walking the
29/// whole table to decide what to drop.
30///
31/// The index holds slot numbers only and compares a probe against the key in
32/// its slot, so each key is stored once: a full text cache's index takes
33/// five bytes an entry instead of a second copy of every key.
34///
35/// The index and slots grow with the entries rather than reserving the bound:
36/// most caches of a process never come near it, and a table sized for
37/// thousands of entries each is megabytes a small screen never touches.
38pub struct BoundedLruCache<K, V> {
39    index: HashTable<u32>,
40    hasher: RandomState,
41    slots: Vec<Option<CacheSlot<K, V>>>,
42    free: Vec<u32>,
43    newest: Option<u32>,
44    oldest: Option<u32>,
45    cap: NonZeroUsize,
46}
47
48/// The occupied slot `slot` of `slots`.
49fn occupied<K, V>(slots: &[Option<CacheSlot<K, V>>], slot: u32) -> &CacheSlot<K, V> {
50    slots[slot as usize]
51        .as_ref()
52        .expect("an indexed or linked cache slot is always occupied")
53}
54
55impl<K, V> BoundedLruCache<K, V>
56where
57    K: Eq + Hash,
58{
59    pub fn new(cap: NonZeroUsize) -> Self {
60        Self {
61            index: HashTable::new(),
62            hasher: RandomState::default(),
63            slots: Vec::new(),
64            free: Vec::new(),
65            newest: None,
66            oldest: None,
67            cap,
68        }
69    }
70
71    pub fn with_capacity_at_least_one(cap: usize) -> Self {
72        let cap = NonZeroUsize::new(cap).unwrap_or(NonZeroUsize::MIN);
73        Self::new(cap)
74    }
75
76    pub fn len(&self) -> usize {
77        self.index.len()
78    }
79
80    pub fn is_empty(&self) -> bool {
81        self.index.is_empty()
82    }
83
84    pub fn cap(&self) -> NonZeroUsize {
85        self.cap
86    }
87
88    /// Sets the bound, evicting the least recently used entries past it.
89    pub fn set_cap(&mut self, cap: NonZeroUsize) {
90        self.cap = cap;
91        while self.len() > cap.get() {
92            self.pop_lru();
93        }
94    }
95
96    /// Drops every entry and gives back the storage they took.
97    pub fn clear(&mut self) {
98        *self = Self::new(self.cap);
99    }
100
101    /// The lookups take any form of the key the stored key borrows as, so a
102    /// caller can probe with a borrowed view instead of building an owned key.
103    pub fn contains<Q>(&self, key: &Q) -> bool
104    where
105        K: Borrow<Q>,
106        Q: Hash + Eq + ?Sized,
107    {
108        self.find(key).is_some()
109    }
110
111    pub fn get<Q>(&mut self, key: &Q) -> Option<&V>
112    where
113        K: Borrow<Q>,
114        Q: Hash + Eq + ?Sized,
115    {
116        let slot = self.find(key)?;
117        self.promote(slot);
118        Some(&self.slot(slot).value)
119    }
120
121    pub fn peek<Q>(&self, key: &Q) -> Option<&V>
122    where
123        K: Borrow<Q>,
124        Q: Hash + Eq + ?Sized,
125    {
126        let slot = self.find(key)?;
127        Some(&self.slot(slot).value)
128    }
129
130    pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
131    where
132        K: Borrow<Q>,
133        Q: Hash + Eq + ?Sized,
134    {
135        let slot = self.find(key)?;
136        self.promote(slot);
137        Some(&mut self.slot_mut(slot).value)
138    }
139
140    pub fn push(&mut self, key: K, value: V) -> Option<(K, V)> {
141        if let Some(slot) = self.find(&key) {
142            self.promote(slot);
143            let old_value = std::mem::replace(&mut self.slot_mut(slot).value, value);
144            return Some((key, old_value));
145        }
146
147        let evicted = if self.index.len() == self.cap.get() {
148            self.pop_lru()
149        } else {
150            None
151        };
152
153        let hash = self.hasher.hash_one(&key);
154        let slot = self.claim_slot(key, value);
155        let (slots, hasher) = (&self.slots, &self.hasher);
156        self.index.insert_unique(hash, slot, |&slot| {
157            hasher.hash_one(&occupied(slots, slot).key)
158        });
159        self.link_newest(slot);
160        evicted
161    }
162
163    pub fn put(&mut self, key: K, value: V) -> Option<V> {
164        self.push(key, value).map(|(_, value)| value)
165    }
166
167    /// The least recently used entry, without touching its recency.
168    pub fn peek_lru(&self) -> Option<(&K, &V)> {
169        let entry = self.slot(self.oldest?);
170        Some((&entry.key, &entry.value))
171    }
172
173    pub fn pop_lru(&mut self) -> Option<(K, V)> {
174        let slot = self.oldest?;
175        let hash = self.hasher.hash_one(&self.slot(slot).key);
176        if let Ok(entry) = self.index.find_entry(hash, |&indexed| indexed == slot) {
177            entry.remove();
178        }
179        self.unlink(slot);
180        let entry = self.release_slot(slot);
181        Some((entry.key, entry.value))
182    }
183
184    pub fn pop(&mut self, key: &K) -> Option<V> {
185        let hash = self.hasher.hash_one(key);
186        let slots = &self.slots;
187        let (slot, _) = self
188            .index
189            .find_entry(hash, |&slot| occupied(slots, slot).key == *key)
190            .ok()?
191            .remove();
192        self.unlink(slot);
193        Some(self.release_slot(slot).value)
194    }
195
196    /// Entries most recently used first.
197    pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
198        let mut next = self.newest;
199        std::iter::from_fn(move || {
200            let entry = self.slot(next?);
201            next = entry.older;
202            Some((&entry.key, &entry.value))
203        })
204    }
205
206    fn find<Q>(&self, key: &Q) -> Option<u32>
207    where
208        K: Borrow<Q>,
209        Q: Hash + Eq + ?Sized,
210    {
211        let slots = &self.slots;
212        self.index
213            .find(self.hasher.hash_one(key), |&slot| {
214                occupied(slots, slot).key.borrow() == key
215            })
216            .copied()
217    }
218
219    fn slot(&self, slot: u32) -> &CacheSlot<K, V> {
220        occupied(&self.slots, slot)
221    }
222
223    fn slot_mut(&mut self, slot: u32) -> &mut CacheSlot<K, V> {
224        self.slots[slot as usize]
225            .as_mut()
226            .expect("a linked cache slot is always occupied")
227    }
228
229    fn promote(&mut self, slot: u32) {
230        if self.newest == Some(slot) {
231            return;
232        }
233        self.unlink(slot);
234        self.link_newest(slot);
235    }
236
237    fn link_newest(&mut self, slot: u32) {
238        let previous_newest = self.newest;
239        {
240            let entry = self.slot_mut(slot);
241            entry.newer = None;
242            entry.older = previous_newest;
243        }
244        if let Some(previous) = previous_newest {
245            self.slot_mut(previous).newer = Some(slot);
246        }
247        self.newest = Some(slot);
248        if self.oldest.is_none() {
249            self.oldest = Some(slot);
250        }
251    }
252
253    fn unlink(&mut self, slot: u32) {
254        let (newer, older) = {
255            let entry = self.slot_mut(slot);
256            (entry.newer.take(), entry.older.take())
257        };
258        match newer {
259            Some(newer) => self.slot_mut(newer).older = older,
260            None => self.newest = older,
261        }
262        match older {
263            Some(older) => self.slot_mut(older).newer = newer,
264            None => self.oldest = newer,
265        }
266    }
267
268    fn claim_slot(&mut self, key: K, value: V) -> u32 {
269        let entry = CacheSlot {
270            key,
271            value,
272            newer: None,
273            older: None,
274        };
275        match self.free.pop() {
276            Some(slot) => {
277                self.slots[slot as usize] = Some(entry);
278                slot
279            }
280            None => {
281                let slot =
282                    u32::try_from(self.slots.len()).expect("a cache holds fewer than 2^32 entries");
283                self.slots.push(Some(entry));
284                slot
285            }
286        }
287    }
288
289    fn release_slot(&mut self, slot: u32) -> CacheSlot<K, V> {
290        let entry = self.slots[slot as usize]
291            .take()
292            .expect("a slot being released is always occupied");
293        self.free.push(slot);
294        entry
295    }
296}
297
298#[cfg(test)]
299#[path = "tests/bounded_lru_tests.rs"]
300mod tests;