cranpose_core/collections/
bounded_lru.rs1use 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
18pub 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
48fn 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 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 pub fn clear(&mut self) {
98 *self = Self::new(self.cap);
99 }
100
101 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 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 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;