Skip to content

Commit

Permalink
Use hashbrown::raw::RawTable for SizedCache
Browse files Browse the repository at this point in the history
Rather than duplicating the keys in `store` and `order`, we can use
`store: RawTable<usize>` that just indexes `order`.
  • Loading branch information
cuviper committed Sep 28, 2020
1 parent 4f2b406 commit edc9781
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 48 deletions.
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
148 changes: 100 additions & 48 deletions src/stores.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -185,7 +188,7 @@ impl<T> LRUList<T> {
self.link_after(index, Self::OCCUPIED);
}

fn push_front(&mut self, value: Option<T>) -> usize {
fn push_front(&mut self, value: T) -> usize {
if self.values[Self::FREE].next == Self::FREE {
self.values.push(ListEntry::<T> {
value: None,
Expand All @@ -195,7 +198,7 @@ impl<T> LRUList<T> {
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
Expand All @@ -211,11 +214,6 @@ impl<T> LRUList<T> {
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")
}
Expand All @@ -225,7 +223,7 @@ impl<T> LRUList<T> {
}

fn set(&mut self, index: usize, value: T) -> Option<T> {
std::mem::replace(&mut self.values[index].value, Some(value))
self.values[index].value.replace(value)
}

fn clear(&mut self) {
Expand Down Expand Up @@ -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<K, V> {
store: HashMap<K, usize>,
store: RawTable<usize>,
hash_builder: RandomState,
order: LRUList<(K, V)>,
capacity: usize,
hits: u64,
misses: u64,
}

impl<K, V> fmt::Debug for SizedCache<K, V>
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<K, V> PartialEq for SizedCache<K, V>
where
K: Eq + Hash,
V: PartialEq,
{
fn eq(&self, other: &SizedCache<K, V>) -> 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,
})
}
}
}

Expand All @@ -315,7 +336,8 @@ impl<K: Hash + Eq, V> SizedCache<K, V> {
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,
Expand All @@ -335,23 +357,56 @@ impl<K: Hash + Eq, V> SizedCache<K, V> {
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<usize> {
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<usize> {
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<K: Hash + Eq + Clone, V> Cached<K, V> for SizedCache<K, V> {
impl<K: Hash + Eq, V> Cached<K, V> for SizedCache<K, V> {
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)
Expand All @@ -364,9 +419,8 @@ impl<K: Hash + Eq + Clone, V> Cached<K, V> for SizedCache<K, V> {
}

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)
Expand All @@ -380,37 +434,35 @@ impl<K: Hash + Eq + Clone, V> Cached<K, V> for SizedCache<K, V> {

fn cache_set(&mut self, key: K, val: V) -> Option<V> {
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<F: FnOnce() -> 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<V> {
// 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)
Expand Down

0 comments on commit edc9781

Please sign in to comment.