diff --git a/Cargo.toml b/Cargo.toml index 0db4984..592c99f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,11 @@ travis-ci = { repository = "jaemk/cached", branch = "master" } default = ["proc_macro"] proc_macro = ["async-mutex", "cached_proc_macro"] +[dependencies.hashbrown] +version = "0.9.1" +default-features = false +features = ["raw"] + [dependencies.once_cell] version = "1" diff --git a/src/stores.rs b/src/stores.rs index ce8f4a8..3520d2a 100644 --- a/src/stores.rs +++ b/src/stores.rs @@ -5,12 +5,15 @@ Implementation of various caches use std::cmp::Eq; use std::collections::HashMap; -use std::hash::Hash; +use std::fmt; +use std::hash::{BuildHasher, Hash, Hasher}; use std::time::Instant; use super::Cached; -use std::collections::hash_map::Entry; +use std::collections::hash_map::{Entry, RandomState}; + +use hashbrown::raw::RawTable; /// Default unbounded cache /// @@ -185,7 +188,7 @@ impl LRUList { self.link_after(index, Self::OCCUPIED); } - fn push_front(&mut self, value: Option) -> usize { + fn push_front(&mut self, value: T) -> usize { if self.values[Self::FREE].next == Self::FREE { self.values.push(ListEntry:: { value: None, @@ -195,7 +198,7 @@ impl LRUList { self.values[Self::FREE].next = self.values.len() - 1; } let index = self.values[Self::FREE].next; - self.values[index].value = value; + self.values[index].value = Some(value); self.unlink(index); self.link_after(index, Self::OCCUPIED); index @@ -211,11 +214,6 @@ impl LRUList { self.values[Self::OCCUPIED].prev } - fn pop_back(&mut self) -> T { - let index = self.back(); - self.remove(index) - } - fn get(&self, index: usize) -> &T { self.values[index].value.as_ref().expect("invalid index") } @@ -225,7 +223,7 @@ impl LRUList { } fn set(&mut self, index: usize, value: T) -> Option { - std::mem::replace(&mut self.values[index].value, Some(value)) + self.values[index].value.replace(value) } fn clear(&mut self) { @@ -277,22 +275,45 @@ impl<'a, T> Iterator for LRUListIterator<'a, T> { /// to evict the least recently used keys /// /// Note: This cache is in-memory only -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct SizedCache { - store: HashMap, + store: RawTable, + hash_builder: RandomState, order: LRUList<(K, V)>, capacity: usize, hits: u64, misses: u64, } +impl fmt::Debug for SizedCache +where + K: fmt::Debug, + V: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SizedCache") + .field("order", &self.order) + .field("capacity", &self.capacity) + .field("hits", &self.hits) + .field("misses", &self.misses) + .finish() + } +} + impl PartialEq for SizedCache where K: Eq + Hash, V: PartialEq, { fn eq(&self, other: &SizedCache) -> bool { - self.store.eq(&other.store) + self.store.len() == other.store.len() && { + self.order + .iter() + .all(|(key, value)| match other.get_index(other.hash(key), key) { + Some(i) => value == &other.order.get(i).1, + None => false, + }) + } } } @@ -315,7 +336,8 @@ impl SizedCache { panic!("`size` of `SizedCache` must be greater than zero.") } SizedCache { - store: HashMap::with_capacity(size), + store: RawTable::with_capacity(size), + hash_builder: RandomState::new(), order: LRUList::<(K, V)>::with_capacity(size), capacity: size, hits: 0, @@ -335,23 +357,56 @@ impl SizedCache { self.order.iter().map(|(_k, v)| v) } + fn hash(&self, key: &K) -> u64 { + let hasher = &mut self.hash_builder.build_hasher(); + key.hash(hasher); + hasher.finish() + } + + fn insert_index(&mut self, hash: u64, index: usize) { + let Self { + ref mut store, + ref order, + ref hash_builder, + .. + } = *self; + store.insert(hash, index, move |&i| { + let hasher = &mut hash_builder.build_hasher(); + order.get(i).0.hash(hasher); + hasher.finish() + }); + } + + fn get_index(&self, hash: u64, key: &K) -> Option { + let Self { store, order, .. } = self; + store.get(hash, |&i| *key == order.get(i).0).copied() + } + + fn remove_index(&mut self, hash: u64, key: &K) -> Option { + let Self { store, order, .. } = self; + store.remove_entry(hash, |&i| *key == order.get(i).0) + } + fn check_capacity(&mut self) { if self.store.len() >= self.capacity { // store has reached capacity, evict the oldest item. // store capacity cannot be zero, so there must be content in `self.order`. - let (key, _value) = self.order.pop_back(); - self.store - .remove(&key) - .expect("SizedCache::cache_set failed evicting cache key"); + let index = self.order.back(); + let key = &self.order.get(index).0; + let hash = self.hash(key); + + let order = &self.order; + let erased = self.store.erase_entry(hash, |&i| *key == order.get(i).0); + assert!(erased, "SizedCache::cache_set failed evicting cache key"); + self.order.remove(index); } } } -impl Cached for SizedCache { +impl Cached for SizedCache { fn cache_get(&mut self, key: &K) -> Option<&V> { - let val = self.store.get(key); - match val { - Some(&index) => { + match self.get_index(self.hash(key), key) { + Some(index) => { self.order.move_to_front(index); self.hits += 1; Some(&self.order.get(index).1) @@ -364,9 +419,8 @@ impl Cached for SizedCache { } fn cache_get_mut(&mut self, key: &K) -> std::option::Option<&mut V> { - let val = self.store.get(key); - match val { - Some(&index) => { + match self.get_index(self.hash(key), key) { + Some(index) => { self.order.move_to_front(index); self.hits += 1; Some(&mut self.order.get_mut(index).1) @@ -380,37 +434,35 @@ impl Cached for SizedCache { fn cache_set(&mut self, key: K, val: V) -> Option { self.check_capacity(); - let Self { store, order, .. } = self; - let index = *store - .entry(key.clone()) - .or_insert_with(|| order.push_front(None)); - order.set(index, (key, val)).map(|(_, v)| v) + let hash = self.hash(&key); + if let Some(index) = self.get_index(hash, &key) { + self.order.set(index, (key, val)).map(|(_, v)| v) + } else { + let index = self.order.push_front((key, val)); + self.insert_index(hash, index); + None + } } fn cache_get_or_set_with V>(&mut self, key: K, f: F) -> &mut V { - self.check_capacity(); - let val = self.store.entry(key); - let Self { order, .. } = self; - match val { - Entry::Occupied(occupied) => { - let index = *occupied.get(); - order.move_to_front(index); - self.hits += 1; - &mut order.get_mut(index).1 - } - Entry::Vacant(vacant) => { - let key = vacant.key().clone(); - self.misses += 1; - let index = *vacant.insert(order.push_front(None)); - order.set(index, (key, f())); - &mut order.get_mut(index).1 - } + let hash = self.hash(&key); + if let Some(index) = self.get_index(hash, &key) { + self.order.move_to_front(index); + self.hits += 1; + &mut self.order.get_mut(index).1 + } else { + self.check_capacity(); + self.misses += 1; + let index = self.order.push_front((key, f())); + self.insert_index(hash, index); + &mut self.order.get_mut(index).1 } } fn cache_remove(&mut self, k: &K) -> Option { // try and remove item from mapping, and then from order list if it was in mapping - if let Some(index) = self.store.remove(k) { + let hash = self.hash(&k); + if let Some(index) = self.remove_index(hash, k) { // need to remove the key in the order list let (_key, value) = self.order.remove(index); Some(value)