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