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