#![no_std]
use core::{borrow::Borrow, cell::Cell};
trait Equivalent<K: ?Sized> {
fn equivalent(&self, k: &K) -> bool;
}
impl<Q: ?Sized, K: ?Sized> Equivalent<K> for Q
where
Q: Eq,
K: Borrow<Q>,
{
fn equivalent(&self, k: &K) -> bool {
self == k.borrow()
}
}
#[derive(Clone, PartialEq)]
enum KeyValueSlot<K, V> {
Used { key: K, value: V, hits: Cell<u8> },
Empty,
}
impl<K, V> KeyValueSlot<K, V> {
#[cfg_attr(feature = "inline-more", inline)]
fn is_key<Q>(&self, k: &Q) -> bool
where
Q: Equivalent<K> + ?Sized,
{
if let KeyValueSlot::Used { key, .. } = self {
k.equivalent(key)
} else {
false
}
}
#[cfg_attr(feature = "inline-more", inline)]
fn get_value(&self) -> Option<&V> {
if let KeyValueSlot::Used { value, hits, .. } = self {
hits.set(hits.get().saturating_add(1));
Some(value)
} else {
None
}
}
#[cfg_attr(feature = "inline-more", inline)]
fn get_value_mut(&mut self) -> Option<&mut V> {
if let KeyValueSlot::Used { value, hits, .. } = self {
hits.set(hits.get().saturating_add(1));
Some(value)
} else {
None
}
}
fn hits(&self) -> u8 {
if let KeyValueSlot::Used { hits, .. } = self {
hits.get()
} else {
0
}
}
#[cfg_attr(feature = "inline-more", inline)]
fn update_value(&mut self, v: V) {
if let KeyValueSlot::Used { value, .. } = self {
*value = v;
}
}
}
pub struct MemoCache<K, V, const SIZE: usize> {
buffer: [KeyValueSlot<K, V>; SIZE],
rng_state: Cell<u32>,
}
impl<K, V, const SIZE: usize> MemoCache<K, V, SIZE>
where
K: Eq,
{
const SIZE_CHECK: () = assert!(SIZE > 0, "Cache size must be greater than 0");
#[cfg_attr(feature = "inline-more", inline)]
#[must_use]
#[allow(clippy::cast_possible_truncation)] pub fn new() -> Self {
let () = Self::SIZE_CHECK;
Self {
buffer: [const { KeyValueSlot::Empty }; SIZE],
rng_state: Cell::new(0x9E37_79B9 ^ (SIZE as u32).wrapping_mul(0x85EB_CA6B)), }
}
#[cfg_attr(feature = "inline-more", inline)]
pub const fn capacity(&self) -> usize {
SIZE
}
const fn eviction_window_size() -> usize {
match SIZE {
0..=4 => SIZE, 5..=16 => SIZE / 2, 17..=64 => 8, _ => 16, }
}
#[cfg_attr(feature = "inline-more", inline)]
fn xorshift32(&self) -> u32 {
let mut x = self.rng_state.get();
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
self.rng_state.set(x);
x
}
fn find_eviction_slot(&self) -> usize {
for (idx, slot) in self.buffer.iter().enumerate() {
if let KeyValueSlot::Empty = slot {
return idx;
}
}
let window_size = Self::eviction_window_size();
let start_idx = ((u64::from(self.xorshift32()) * SIZE as u64) >> 32) as usize;
let mut evict_idx = start_idx;
let mut min_hits = u8::MAX;
for i in 0..window_size {
let idx = (start_idx + i) % SIZE;
let hits = unsafe { self.buffer.get_unchecked(idx) }.hits();
if hits < min_hits {
min_hits = hits;
evict_idx = idx;
if hits == 0 {
break;
}
}
}
evict_idx
}
#[cfg_attr(feature = "inline-more", inline)]
fn evict_and_replace(&mut self, k: K, v: V) -> &V {
self.decay_hits();
let idx = self.find_eviction_slot();
let s = unsafe { self.buffer.get_unchecked_mut(idx) };
*s = KeyValueSlot::Used {
key: k,
value: v,
hits: Cell::new(0),
};
unsafe { s.get_value().unwrap_unchecked() }
}
fn decay_hits(&mut self) {
const DECAY_THRESHOLD: u8 = 192;
let decay_needed = self
.buffer
.iter()
.any(|s| matches!(s, KeyValueSlot::Used { hits, .. } if hits.get() >= DECAY_THRESHOLD));
if decay_needed {
for s in &mut self.buffer {
if let KeyValueSlot::Used { hits, .. } = s {
let current = hits.get();
hits.set(current - (current >> 2)); }
}
}
}
#[cfg_attr(feature = "inline-more", inline)]
pub fn insert(&mut self, k: K, v: V) {
match self.buffer.iter_mut().find(|e| e.is_key(&k)) {
Some(s) => s.update_value(v),
None => {
self.evict_and_replace(k, v);
}
}
}
#[cfg_attr(feature = "inline-more", inline)]
pub fn contains_key<Q>(&self, k: &Q) -> bool
where
K: Borrow<Q>,
Q: Eq + ?Sized,
{
self.buffer.iter().any(|e| e.is_key(k))
}
#[cfg_attr(feature = "inline-more", inline)]
pub fn get<Q>(&self, k: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Eq + ?Sized,
{
self.buffer.iter().find(|e| e.is_key(k)).map(|e| {
debug_assert!(matches!(e, KeyValueSlot::Used { .. }), "Expected used slot");
unsafe { e.get_value().unwrap_unchecked() }
})
}
#[cfg_attr(feature = "inline-more", inline)]
pub fn get_mut<Q>(&mut self, k: &Q) -> Option<&mut V>
where
K: Borrow<Q>,
Q: Eq + ?Sized,
{
self.buffer.iter_mut().find(|e| e.is_key(k)).map(|e| {
debug_assert!(matches!(e, KeyValueSlot::Used { .. }), "Expected used slot");
unsafe { e.get_value_mut().unwrap_unchecked() }
})
}
#[cfg_attr(feature = "inline-more", inline)]
fn get_key_index<Q>(&self, k: &Q) -> Option<usize>
where
K: Borrow<Q>,
Q: Eq + ?Sized,
{
self.buffer.iter().position(|e| e.is_key(k))
}
#[cfg_attr(feature = "inline-more", inline)]
pub fn get_or_insert_with<F>(&mut self, k: &K, f: F) -> &V
where
K: Clone,
F: FnOnce(&K) -> V,
{
if let Some(i) = self.get_key_index(k) {
unsafe { self.buffer.get_unchecked(i).get_value().unwrap_unchecked() }
} else {
self.evict_and_replace(k.clone(), f(k))
}
}
#[cfg_attr(feature = "inline-more", inline)]
pub fn get_or_try_insert_with<F, E>(&mut self, k: &K, f: F) -> Result<&V, E>
where
K: Clone,
F: FnOnce(&K) -> Result<V, E>,
{
if let Some(i) = self.get_key_index(k) {
Ok(unsafe { self.buffer.get_unchecked(i).get_value().unwrap_unchecked() })
} else {
f(k).map(|v| self.evict_and_replace(k.clone(), v))
}
}
#[cfg_attr(feature = "inline-more", inline)]
pub fn clear(&mut self) {
self.buffer
.iter_mut()
.for_each(|e| *e = KeyValueSlot::Empty);
}
}
impl<K, V, const SIZE: usize> Default for MemoCache<K, V, SIZE>
where
K: Eq,
{
fn default() -> Self {
Self::new()
}
}
impl<K, V, const SIZE: usize> Clone for MemoCache<K, V, SIZE>
where
K: Eq + Clone,
V: Clone,
{
fn clone(&self) -> Self {
Self {
buffer: self.buffer.clone(),
rng_state: self.rng_state.clone(),
}
}
}
#[cfg(test)]
mod tests_internal {
use super::*;
#[test]
fn test_new_state() {
const SIZE: usize = 8;
let c = MemoCache::<i32, i32, SIZE>::new();
assert_eq!(c.buffer.len(), SIZE);
assert_eq!(c.capacity(), SIZE);
assert!(c.buffer.iter().all(|s| s == &KeyValueSlot::Empty));
}
const _: fn() = || {
fn assert_send<T: Send>() {}
assert_send::<MemoCache<i32, i32, 4>>();
};
}