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