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}