#![no_std]
use core::{borrow::Borrow, cell::Cell, fmt, mem};
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()
}
}
const DECAY_THRESHOLD: u8 = 192;
#[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, decay_pending: &Cell<bool>) -> Option<&V> {
if let KeyValueSlot::Used { value, hits, .. } = self {
let h = hits.get().saturating_add(1);
hits.set(h);
if h == DECAY_THRESHOLD {
decay_pending.set(true);
}
Some(value)
} else {
None
}
}
#[cfg_attr(feature = "inline-more", inline)]
fn get_value_mut(&mut self, decay_pending: &Cell<bool>) -> Option<&mut V> {
if let KeyValueSlot::Used { value, hits, .. } = self {
let h = hits.get().saturating_add(1);
hits.set(h);
if h == DECAY_THRESHOLD {
decay_pending.set(true);
}
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) -> Option<V> {
if let KeyValueSlot::Used { value, .. } = self {
Some(mem::replace(value, v))
} else {
None
}
}
}
pub struct MemoCache<K, V, const SIZE: usize> {
buffer: [KeyValueSlot<K, V>; SIZE],
rng_state: Cell<u32>,
used: usize,
decay_pending: Cell<bool>,
}
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)), used: 0,
decay_pending: Cell::new(false),
}
}
#[cfg_attr(feature = "inline-more", inline)]
pub const fn capacity(&self) -> usize {
SIZE
}
#[cfg_attr(feature = "inline-more", inline)]
pub const fn len(&self) -> usize {
self.used
}
#[cfg_attr(feature = "inline-more", inline)]
pub const fn is_empty(&self) -> bool {
self.used == 0
}
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 {
if self.used < SIZE {
if let Some(idx) = self
.buffer
.iter()
.position(|s| matches!(s, KeyValueSlot::Empty))
{
return idx;
}
debug_assert!(false, "Occupancy count says an empty slot exists");
}
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 = self.buffer[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();
if self.used < SIZE {
self.used += 1;
}
let s = &mut self.buffer[idx];
*s = KeyValueSlot::Used {
key: k,
value: v,
hits: Cell::new(0),
};
match s {
KeyValueSlot::Used { value, .. } => value,
KeyValueSlot::Empty => unreachable!(), }
}
fn decay_hits(&mut self) {
if !self.decay_pending.get() {
return;
}
let true_max = self
.buffer
.iter()
.map(KeyValueSlot::hits)
.max()
.unwrap_or(0);
if true_max >= DECAY_THRESHOLD {
for s in &mut self.buffer {
if let KeyValueSlot::Used { hits, .. } = s {
let current = hits.get();
hits.set(current - (current >> 2)); }
}
self.decay_pending
.set(true_max - (true_max >> 2) >= DECAY_THRESHOLD);
} else {
self.decay_pending.set(false);
}
}
#[cfg_attr(feature = "inline-more", inline)]
pub fn insert(&mut self, k: K, v: V) -> Option<V> {
if let Some(s) = self.buffer.iter_mut().find(|e| e.is_key(&k)) {
s.update_value(v)
} else {
self.evict_and_replace(k, v);
None
}
}
#[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))
.and_then(|e| e.get_value(&self.decay_pending))
}
#[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,
{
let decay_pending = &self.decay_pending;
self.buffer
.iter_mut()
.find(|e| e.is_key(k))
.and_then(|e| e.get_value_mut(decay_pending))
}
#[cfg_attr(feature = "inline-more", inline)]
pub fn remove<Q>(&mut self, k: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Eq + ?Sized,
{
let used = &mut self.used;
self.buffer.iter_mut().find(|e| e.is_key(k)).map(|s| {
*used -= 1;
match mem::replace(s, KeyValueSlot::Empty) {
KeyValueSlot::Used { value, .. } => value,
KeyValueSlot::Empty => unreachable!(), }
})
}
#[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) {
self.buffer[i]
.get_value(&self.decay_pending)
.expect("Slot found by key must be used")
} 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(self.buffer[i]
.get_value(&self.decay_pending)
.expect("Slot found by key must be used"))
} else {
f(k).map(|v| self.evict_and_replace(k.clone(), v))
}
}
#[cfg_attr(feature = "inline-more", inline)]
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
self.buffer.iter().filter_map(|s| match s {
KeyValueSlot::Used { key, value, .. } => Some((key, value)),
KeyValueSlot::Empty => None,
})
}
#[cfg_attr(feature = "inline-more", inline)]
pub fn clear(&mut self) {
self.buffer
.iter_mut()
.for_each(|e| *e = KeyValueSlot::Empty);
self.used = 0;
self.decay_pending.set(false);
}
}
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> fmt::Debug for MemoCache<K, V, SIZE>
where
K: Eq + fmt::Debug,
V: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_map().entries(self.iter()).finish()
}
}
impl<K, V, const SIZE: usize> Clone for MemoCache<K, V, SIZE>
where
K: Eq + Clone,
V: Clone,
{
fn clone(&self) -> Self {
let mut rng_state = self.rng_state.get() ^ 0x5851_F42D; if rng_state == 0 {
rng_state = 0x9E37_79B9; }
Self {
buffer: self.buffer.clone(),
rng_state: Cell::new(rng_state),
used: self.used,
decay_pending: self.decay_pending.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>>();
};
}