memo_cache/lib.rs
1#![no_std]
2
3use core::{borrow::Borrow, cell::Cell};
4
5/// Key equivalence trait, to support `Borrow` types as keys.
6trait Equivalent<K: ?Sized> {
7 /// Returns `true` if two values are equivalent, `false` otherwise.
8 fn equivalent(&self, k: &K) -> bool;
9}
10
11impl<Q: ?Sized, K: ?Sized> Equivalent<K> for Q
12where
13 Q: Eq,
14 K: Borrow<Q>,
15{
16 fn equivalent(&self, k: &K) -> bool {
17 self == k.borrow()
18 }
19}
20
21/// A single key/value slot used in the cache.
22#[derive(Clone, PartialEq)]
23enum KeyValueSlot<K, V> {
24 Used { key: K, value: V, hits: Cell<u8> },
25 Empty,
26}
27
28impl<K, V> KeyValueSlot<K, V> {
29 /// Check a used slot key for equivalence.
30 #[cfg_attr(feature = "inline-more", inline)]
31 fn is_key<Q>(&self, k: &Q) -> bool
32 where
33 Q: Equivalent<K> + ?Sized,
34 {
35 if let KeyValueSlot::Used { key, .. } = self {
36 k.equivalent(key)
37 } else {
38 false
39 }
40 }
41
42 /// Get the value of a used slot.
43 #[cfg_attr(feature = "inline-more", inline)]
44 fn get_value(&self) -> Option<&V> {
45 if let KeyValueSlot::Used { value, hits, .. } = self {
46 hits.set(hits.get().saturating_add(1));
47 Some(value)
48 } else {
49 None
50 }
51 }
52
53 /// Get the value of a used slot (for mutation).
54 #[cfg_attr(feature = "inline-more", inline)]
55 fn get_value_mut(&mut self) -> Option<&mut V> {
56 if let KeyValueSlot::Used { value, hits, .. } = self {
57 hits.set(hits.get().saturating_add(1));
58 Some(value)
59 } else {
60 None
61 }
62 }
63
64 /// Get the number of cache hits for this slot.
65 fn hits(&self) -> u8 {
66 if let KeyValueSlot::Used { hits, .. } = self {
67 hits.get()
68 } else {
69 0
70 }
71 }
72
73 /// Update the value of a used slot.
74 #[cfg_attr(feature = "inline-more", inline)]
75 fn update_value(&mut self, v: V) {
76 if let KeyValueSlot::Used { value, .. } = self {
77 *value = v;
78 }
79 }
80}
81
82/// A small, fixed-size, heap-allocated key/value cache with retention management.
83pub struct MemoCache<K, V, const SIZE: usize> {
84 buffer: [KeyValueSlot<K, V>; SIZE],
85 rng_state: Cell<u32>,
86}
87
88impl<K, V, const SIZE: usize> MemoCache<K, V, SIZE>
89where
90 K: Clone + Eq,
91 V: Clone,
92{
93 /// Create a new cache.
94 ///
95 /// # Examples
96 ///
97 /// ```
98 /// use memo_cache::MemoCache;
99 ///
100 /// let c = MemoCache::<u32, String, 4>::new();
101 /// ```
102 #[cfg_attr(feature = "inline-more", inline)]
103 #[must_use]
104 pub fn new() -> Self {
105 assert!(SIZE > 0, "Cache size must be greater than 0");
106
107 Self {
108 buffer: [const { KeyValueSlot::Empty }; SIZE],
109 rng_state: Cell::new(0x9E37_79B9), // The golden ratio prime.
110 }
111 }
112
113 /// Get the (fixed) capacity of the cache in [number of elements].
114 ///
115 /// # Examples
116 ///
117 /// ```
118 /// use memo_cache::MemoCache;
119 ///
120 /// let c = MemoCache::<u32, String, 8>::new();
121 ///
122 /// assert_eq!(c.capacity(), 8);
123 /// ```
124 #[cfg_attr(feature = "inline-more", inline)]
125 pub const fn capacity(&self) -> usize {
126 SIZE
127 }
128
129 /// Get the size of the cache slot eviction search window.
130 const fn eviction_window_size() -> usize {
131 match SIZE {
132 0..=4 => SIZE, // Tiny cache.
133 5..=16 => SIZE / 2, // Small-sized cache.
134 17..=64 => 8, // Medium-sized cache.
135 _ => 16, // Large cache.
136 }
137 }
138
139 /// Generate a pseudo-random value using Xorshift32 (see: <https://en.wikipedia.org/wiki/Xorshift>).
140 #[cfg_attr(feature = "inline-more", inline)]
141 fn xorshift32(&self) -> u32 {
142 let mut x = self.rng_state.get();
143 x ^= x << 13;
144 x ^= x >> 17;
145 x ^= x << 5;
146 self.rng_state.set(x);
147 x
148 }
149
150 /// Find a cache slot index to evict for replacement.
151 fn find_eviction_slot(&self) -> usize {
152 // Check for empty slots first.
153 for (idx, slot) in self.buffer.iter().enumerate() {
154 if let KeyValueSlot::Empty = slot {
155 return idx;
156 }
157 }
158
159 // The cache is fully occupied, find a slot to evict by scanning a window at a random position.
160 let window_size = Self::eviction_window_size();
161 let start_idx = ((u64::from(self.xorshift32()) * SIZE as u64) >> 32) as usize;
162
163 let mut evict_idx = start_idx;
164 let mut min_hits = u8::MAX;
165
166 for i in 0..window_size {
167 let idx = (start_idx + i) % SIZE;
168 // SAFETY: idx is guaranteed to be < SIZE due to the modulo operation.
169 let hits = unsafe { self.buffer.get_unchecked(idx) }.hits();
170
171 if hits < min_hits {
172 min_hits = hits;
173 evict_idx = idx;
174
175 // Early-out for unhit cache entries.
176 if hits == 0 {
177 break;
178 }
179 }
180 }
181
182 evict_idx
183 }
184
185 /// Evict a suitable slot and replace it. Returns a reference to the replaced slot value.
186 #[cfg_attr(feature = "inline-more", inline)]
187 fn evict_and_replace(&mut self, k: K, v: V) -> &V {
188 let idx = self.find_eviction_slot();
189 let s = unsafe { self.buffer.get_unchecked_mut(idx) };
190
191 *s = KeyValueSlot::Used {
192 key: k,
193 value: v,
194 hits: Cell::new(0),
195 };
196
197 // SAFETY: The slot was filled with a key/value above.
198 unsafe { s.get_value().unwrap_unchecked() }
199 }
200
201 /// Decay hit values for all occupied slots if any slot reaches the maximum value.
202 fn decay_hits(&mut self) {
203 let decay_needed = self.buffer.iter().any(|s| {
204 if let KeyValueSlot::Used { hits, .. } = s {
205 hits.get() == u8::MAX
206 } else {
207 false
208 }
209 });
210
211 if decay_needed {
212 for s in &mut self.buffer {
213 if let KeyValueSlot::Used { hits, .. } = s {
214 hits.set(hits.get() >> 1); // Divide by two.
215 }
216 }
217 }
218 }
219
220 /// Insert a key/value pair.
221 ///
222 /// # Examples
223 ///
224 /// ```
225 /// use memo_cache::MemoCache;
226 ///
227 /// let mut c = MemoCache::<u32, &str, 4>::new();
228 ///
229 /// assert_eq!(c.get(&42), None);
230 ///
231 /// c.insert(42, "The Answer");
232 ///
233 /// assert_eq!(c.get(&42), Some(&"The Answer"));
234 /// ```
235 #[cfg_attr(feature = "inline-more", inline)]
236 pub fn insert(&mut self, k: K, v: V) {
237 self.decay_hits();
238
239 match self.buffer.iter_mut().find(|e| e.is_key(&k)) {
240 Some(s) => s.update_value(v),
241 None => {
242 self.evict_and_replace(k, v);
243 }
244 }
245 }
246
247 /// Returns `true` if the cache contains a value for the specified key.
248 ///
249 /// # Examples
250 ///
251 /// ```
252 /// use memo_cache::MemoCache;
253 ///
254 /// let mut c = MemoCache::<u32, &str, 4>::new();
255 ///
256 /// assert_eq!(c.contains_key(&42), false);
257 ///
258 /// c.insert(42, "The Answer");
259 ///
260 /// assert_eq!(c.contains_key(&42), true);
261 /// ```
262 #[cfg_attr(feature = "inline-more", inline)]
263 pub fn contains_key<Q>(&self, k: &Q) -> bool
264 where
265 K: Borrow<Q>,
266 Q: Eq + ?Sized,
267 {
268 self.buffer.iter().any(|e| e.is_key(k))
269 }
270
271 /// Lookup a cache entry by key.
272 ///
273 /// # Examples
274 ///
275 /// ```
276 /// use memo_cache::MemoCache;
277 ///
278 /// let mut c = MemoCache::<u32, &str, 4>::new();
279 ///
280 /// assert_eq!(c.get(&42), None);
281 ///
282 /// c.insert(42, "The Answer");
283 ///
284 /// assert_eq!(c.get(&42), Some(&"The Answer"));
285 /// ```
286 #[cfg_attr(feature = "inline-more", inline)]
287 pub fn get<Q>(&self, k: &Q) -> Option<&V>
288 where
289 K: Borrow<Q>,
290 Q: Eq + ?Sized,
291 {
292 self.buffer
293 .iter()
294 .find(|e| e.is_key(k))
295 // SAFETY: We must unwrap the `Option` we get from the cache.
296 .map(|e| unsafe { e.get_value().unwrap_unchecked() })
297 }
298
299 /// Lookup a cache entry by key (for mutation).
300 ///
301 /// # Examples
302 ///
303 /// ```
304 /// use memo_cache::MemoCache;
305 ///
306 /// let mut c = MemoCache::<u32, &str, 4>::new();
307 ///
308 /// c.insert(42, "The Answer");
309 ///
310 /// if let Some(v) = c.get_mut(&42) {
311 /// *v = "Another Answer";
312 /// }
313 ///
314 /// assert_eq!(c.get(&42), Some(&"Another Answer"));
315 /// ```
316 #[cfg_attr(feature = "inline-more", inline)]
317 pub fn get_mut<Q>(&mut self, k: &Q) -> Option<&mut V>
318 where
319 K: Borrow<Q>,
320 Q: Eq + ?Sized,
321 {
322 self.buffer
323 .iter_mut()
324 .find(|e| e.is_key(k))
325 // SAFETY: We must unwrap the `Option` we get from the cache.
326 .map(|e| unsafe { e.get_value_mut().unwrap_unchecked() })
327 }
328
329 /// Get the index for a given key, if found.
330 #[cfg_attr(feature = "inline-more", inline)]
331 fn get_key_index<Q>(&self, k: &Q) -> Option<usize>
332 where
333 K: Borrow<Q>,
334 Q: Eq + ?Sized,
335 {
336 self.buffer.iter().position(|e| e.is_key(k))
337 }
338
339 /// Get a value, or, if it does not exist in the cache, insert it using the value computed by `f`.
340 /// Returns a reference to the found, or newly inserted value associated with the given key.
341 /// If a value is inserted, the key is cloned.
342 ///
343 /// # Examples
344 ///
345 /// ```
346 /// use memo_cache::MemoCache;
347 ///
348 /// let mut c = MemoCache::<u32, &str, 4>::new();
349 ///
350 /// assert_eq!(c.get(&42), None);
351 ///
352 /// let v = c.get_or_insert_with(&42, |_| "The Answer");
353 ///
354 /// assert_eq!(v, &"The Answer");
355 /// assert_eq!(c.get(&42), Some(&"The Answer"));
356 /// ```
357 ///
358 /// # Notes
359 ///
360 /// Because this crate is `no_std`, we have no access to `std::borrow::ToOwned`, which means we cannot create a
361 /// version of `get_or_insert_with` that can create an owned value from a borrowed key.
362 ///
363 #[cfg_attr(feature = "inline-more", inline)]
364 pub fn get_or_insert_with<F>(&mut self, k: &K, f: F) -> &V
365 where
366 F: FnOnce(&K) -> V,
367 {
368 if let Some(i) = self.get_key_index(k) {
369 // SAFETY: The key index was retrieved from a found key.
370 unsafe { self.buffer.get_unchecked(i).get_value().unwrap_unchecked() }
371 } else {
372 self.evict_and_replace(k.clone(), f(k))
373 }
374 }
375
376 /// Get a value, or, if it does not exist in the cache, insert it using the value computed by `f`.
377 /// Returns a result with a reference to the found, or newly inserted value associated with the given key.
378 /// If a value is inserted, the key is cloned.
379 ///
380 /// # Examples
381 ///
382 /// ```
383 /// use memo_cache::MemoCache;
384 ///
385 /// let mut c = MemoCache::<u32, &str, 4>::new();
386 ///
387 /// assert_eq!(c.get(&42), None);
388 ///
389 /// let answer : Result<_, &str> = Ok("The Answer");
390 /// let v = c.get_or_try_insert_with(&42, |_| answer);
391 ///
392 /// assert_eq!(v, Ok(&"The Answer"));
393 /// assert_eq!(c.get(&42), Some(&"The Answer"));
394 ///
395 /// let v = c.get_or_try_insert_with(&17, |_| Err("Dunno"));
396 ///
397 /// assert_eq!(v, Err("Dunno"));
398 /// assert_eq!(c.get(&17), None);
399 /// ```
400 ///
401 /// # Errors
402 ///
403 /// If the function `f` fails, the error of type `E` is returned.
404 ///
405 /// # Notes
406 ///
407 /// Because this crate is `no_std`, we have no access to `std::borrow::ToOwned`, which means we cannot create a
408 /// version of `get_or_try_insert_with` that can create an owned value from a borrowed key.
409 ///
410 #[cfg_attr(feature = "inline-more", inline)]
411 pub fn get_or_try_insert_with<F, E>(&mut self, k: &K, f: F) -> Result<&V, E>
412 where
413 F: FnOnce(&K) -> Result<V, E>,
414 {
415 if let Some(i) = self.get_key_index(k) {
416 // SAFETY: The key index was retrieved from a found key.
417 Ok(unsafe { self.buffer.get_unchecked(i).get_value().unwrap_unchecked() })
418 } else {
419 f(k).map(|v| self.evict_and_replace(k.clone(), v))
420 }
421 }
422
423 /// Clear the cache.
424 ///
425 /// # Examples
426 ///
427 /// ```
428 /// use memo_cache::MemoCache;
429 ///
430 /// let mut c = MemoCache::<u32, &str, 4>::new();
431 ///
432 /// assert_eq!(c.get(&42), None);
433 ///
434 /// c.insert(42, "The Answer");
435 ///
436 /// assert_eq!(c.get(&42), Some(&"The Answer"));
437 ///
438 /// c.clear();
439 ///
440 /// assert_eq!(c.get(&42), None);
441 ///
442 #[cfg_attr(feature = "inline-more", inline)]
443 pub fn clear(&mut self) {
444 self.buffer
445 .iter_mut()
446 .for_each(|e| *e = KeyValueSlot::Empty);
447 }
448}
449
450impl<K, V, const SIZE: usize> Default for MemoCache<K, V, SIZE>
451where
452 K: Clone + Eq,
453 V: Clone,
454{
455 fn default() -> Self {
456 Self::new()
457 }
458}
459
460#[cfg(test)]
461mod tests_internal {
462 use super::*;
463
464 #[test]
465 fn test_new_state() {
466 const SIZE: usize = 8;
467
468 let c = MemoCache::<i32, i32, SIZE>::new();
469
470 // Verify cache size.
471 assert_eq!(c.buffer.len(), SIZE);
472 assert_eq!(c.capacity(), SIZE);
473
474 // All slots should be empty.
475 assert!(c.buffer.iter().all(|s| s == &KeyValueSlot::Empty));
476 }
477}