Skip to content

Commit

Permalink
Merge pull request #7 from arindas/develop
Browse files Browse the repository at this point in the history
docs: adds documentation for arena, list and lru_cache modules
  • Loading branch information
arindas authored Feb 7, 2024
2 parents 9b80455 + 2c89acb commit 6d9f568
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 37 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ license = "MIT"
keywords = ["cache", "lru-cache", "generational-arena", "no-std"]
categories = ["algorithms", "caching"]
exclude = [".github/"]
version = "0.2.0"
version = "0.2.1"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
70 changes: 37 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,67 +27,71 @@ Generational Arena based cache impls. in 100% safe, [no_std] compatible Rust.

```toml
[dependencies]
generational-cache = "0.2.0"
generational-cache = "0.2.1"
```

Refer to latest git [API Documentation](https://arindas.github.io/generational-cache/docs/generational_cache/)
or [Crate Documentation](https://docs.rs/generational-cache) for more details.

### Examples

#### `#1`: Generational arena based LRU cache implementation
```rust
#[no_std]
1. LRU Cache (`generational_cache::cache::LRUCache`)

use generational_cache::prelude::*;
A generational arena based LRU cache implementation.

const CAPACITY: usize = 3;
```rust
#[no_std]

// users can choose between different map and vector implementations
let mut cache = LRUCache::<_, i32, u64, AllocBTreeMap<_, _>>::with_backing_vector(Array::<_, CAPACITY>::new());
use generational_cache::prelude::*;

cache.insert(-1, 1).unwrap();
cache.insert(-2, 2).unwrap();
cache.insert(-3, 3).unwrap();
const CAPACITY: usize = 3;

assert_eq!(cache.least_recent().unwrap(), (&-1, &1));
assert_eq!(cache.most_recent().unwrap(), (&-3, &3));
// users can choose between different map and vector implementations
let mut cache = LRUCache::<_, i32, u64, AllocBTreeMap<_, _>>::with_backing_vector(Array::<_, CAPACITY>::new());

assert_eq!(cache.insert(-4, 4).unwrap(), Eviction::Block { key: -1, value: 1});
cache.insert(-1, 1).unwrap();
cache.insert(-2, 2).unwrap();
cache.insert(-3, 3).unwrap();

assert_eq!(cache.least_recent().unwrap(), (&-2, &2));
assert_eq!(cache.most_recent().unwrap(), (&-4, &4));
assert_eq!(cache.least_recent().unwrap(), (&-1, &1));
assert_eq!(cache.most_recent().unwrap(), (&-3, &3));

assert_eq!(cache.insert(-2, 42).unwrap(), Eviction::Value(2));
assert_eq!(cache.insert(-4, 4).unwrap(), Eviction::Block { key: -1, value: 1});

assert_eq!(cache.least_recent().unwrap(), (&-3, &3));
assert_eq!(cache.most_recent().unwrap(), (&-2, &42));
assert_eq!(cache.least_recent().unwrap(), (&-2, &2));
assert_eq!(cache.most_recent().unwrap(), (&-4, &4));

assert_eq!(cache.remove(&-42).unwrap(), Lookup::Miss);
assert_eq!(cache.query(&-42).unwrap(), Lookup::Miss);
assert_eq!(cache.insert(-2, 42).unwrap(), Eviction::Value(2));

assert_eq!(cache.query(&-3).unwrap(), Lookup::Hit(&3));
assert_eq!(cache.least_recent().unwrap(), (&-3, &3));
assert_eq!(cache.most_recent().unwrap(), (&-2, &42));

assert_eq!(cache.least_recent().unwrap(), (&-4, &4));
assert_eq!(cache.most_recent().unwrap(), (&-3, &3));
assert_eq!(cache.remove(&-42).unwrap(), Lookup::Miss);
assert_eq!(cache.query(&-42).unwrap(), Lookup::Miss);

assert_eq!(cache.remove(&-2).unwrap(), Lookup::Hit(42));
assert_eq!(cache.query(&-3).unwrap(), Lookup::Hit(&3));

assert_eq!(cache.query(&-2).unwrap(), Lookup::Miss);
assert_eq!(cache.least_recent().unwrap(), (&-4, &4));
assert_eq!(cache.most_recent().unwrap(), (&-3, &3));

// zero capacity LRUCache is unusable
let mut cache = LRUCache::<_, i32, u64, AllocBTreeMap<_, _>>::with_backing_vector(Array::<_, 0_usize>::new());
assert_eq!(cache.remove(&-2).unwrap(), Lookup::Hit(42));

match cache.insert(0, 0) {
Err(LRUCacheError::ListUnderflow) => {}
_ => unreachable!("Wrong error on list underflow."),
};
assert_eq!(cache.query(&-2).unwrap(), Lookup::Miss);

```
// zero capacity LRUCache is unusable
let mut cache = LRUCache::<_, i32, u64, AllocBTreeMap<_, _>>::with_backing_vector(Array::<_, 0_usize>::new());

match cache.insert(0, 0) {
Err(LRUCacheError::ListUnderflow) => {}
_ => unreachable!("Wrong error on list underflow."),
};

```

(… we plan on adding more cache implementations in the future).

## License

This repository is licensed under the MIT License. See
[License](https://raw.githubusercontent.com/arindas/generational-cache/main/LICENSE)
for more details.
24 changes: 23 additions & 1 deletion src/arena/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,26 @@ use core::{
marker::PhantomData,
};

/// An generational counter augemented index to track entries.
/// A generational counter augemented index to track arena allocation entries.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Index {
/// Generation counter.
pub generation: u64,

/// Index to the [`Vector`] impl. instance underlying an [`Arena`].
pub idx: usize,
}

/// An allocation entry in a generational arena.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Entry<T> {
/// An occupied entry containing an allocated value and the associated generation counter.
Occupied { value: T, generation: u64 },

/// Free entry pointing to next free entry in the free list.
Free { next_free_idx: Option<usize> },

/// An unmapped arena entry.
Unmapped,
}

Expand Down Expand Up @@ -86,10 +94,17 @@ pub struct Arena<V, T> {
_phantom_type: PhantomData<T>,
}

/// Error type associated with arena operations.
#[derive(Debug)]
pub enum ArenaError<VE> {
/// Used on inserts on a maxed out [`Arena`] which is out of memory.
OutOfMemory,

/// Used when referencing items in an [`Arena`] with an invalid [`Index`].
InvalidIdx,

/// Used when there is an error in the underlying [`Vector`]
/// implemenation instance.
VectorError(VE),
}

Expand All @@ -107,6 +122,7 @@ impl<V, T> Arena<V, T>
where
V: Vector<Entry<T>>,
{
/// Reserves space for the given number of additional items in this arena.
pub fn reserve(&mut self, additional: usize) -> Result<(), ArenaError<V::Error>> {
let reserve_start = self.entries_vec.len();
let old_head = self.free_list_head;
Expand Down Expand Up @@ -137,6 +153,7 @@ where
Ok(())
}

/// Removes all items from this arena and reclaims all allocated memory.
pub fn clear(&mut self) -> Result<(), ArenaError<V::Error>> {
self.free_list_head = Some(0);
self.generation = 0;
Expand All @@ -163,6 +180,8 @@ where
Ok(())
}

/// Creates an [`Arena`] instance with the given [`Vector`]
/// implemenation instance as the backing memory.
pub fn with_vector(vector: V) -> Self {
let capacity = vector.capacity();

Expand All @@ -180,6 +199,7 @@ where
arena
}

/// Allocates space for the given items and inserts it into this arena.
pub fn insert(&mut self, item: T) -> Result<Index, ArenaError<V::Error>> {
let old_free = self.free_list_head.ok_or(ArenaError::OutOfMemory)?;

Expand Down Expand Up @@ -211,6 +231,8 @@ where
})
}

/// Reclaims the allocated space for the item at the given index and
/// removes it from the arena.
pub fn remove(&mut self, index: &Index) -> Option<T> {
match self.entries_vec.get(index.idx) {
Some(Entry::Occupied {
Expand Down
17 changes: 17 additions & 0 deletions src/cache/lru_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ pub struct Block<K, T> {
pub type LRUCacheBlockArenaEntry<K, T> = LinkedListArenaEntry<Block<K, T>>;

/// A [generational-arena](crate::arena::Arena) powered LRUCache implementation.
///
/// This [`Cache`] implementation always evicts the least-recently-used (LRU) key/value pair.
pub struct LRUCache<V, K, T, M> {
block_list: LinkedList<V, Block<K, T>>,
block_refs: M,
Expand All @@ -86,11 +88,13 @@ where
V: Vector<LRUCacheBlockArenaEntry<K, T>>,
M: Map<K, Link>,
{
/// Returns the least recently used key/value pair.
pub fn least_recent(&self) -> Option<(&K, &T)> {
let block = self.block_list.peek_front()?;
Some((&block.key, &block.value))
}

/// Returns the most recently used key/value pair.
pub fn most_recent(&self) -> Option<(&K, &T)> {
let block = self.block_list.peek_back()?;
Some((&block.key, &block.value))
Expand All @@ -102,6 +106,8 @@ where
V: Vector<LRUCacheBlockArenaEntry<K, T>>,
M: Map<K, Link>,
{
/// Creates an [`LRUCache`] instance with the given backing [`Vector`] and [`Map`]
/// implementation instances.
pub fn with_backing_vector_and_map(vector: V, map: M) -> Self {
let block_list = LinkedList::with_backing_vector(vector);
let capacity = block_list.capacity();
Expand All @@ -119,6 +125,8 @@ where
V: Vector<LRUCacheBlockArenaEntry<K, T>>,
M: Map<K, Link> + Default,
{
/// Creates an [`LRUCache`] instance with the given [`Vector`] implementation instance
/// and the default [`Map`] implementation value.
pub fn with_backing_vector(vector: V) -> Self {
Self::with_backing_vector_and_map(vector, M::default())
}
Expand All @@ -134,11 +142,20 @@ where
}
}

/// Error type associated with [`LRUCache`] operations.
#[derive(Debug)]
pub enum LRUCacheError<VE, ME> {
/// Used when there is an error on an operation in the underlying list.
ListError(ListError<VE>),

/// Used when attempting to remove elements from the underlying list when its empty.
ListUnderflow,

/// Used when the underlying map and list instances contain an inconsistent view
/// of the entries allocated in the LRUCache
MapListInconsistent,

/// Used when there is an error on an operation in the underlying map..
MapError(ME),
}

Expand Down
33 changes: 31 additions & 2 deletions src/cache/mod.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,74 @@
//! Module providing abstractions to represent caches.

/// An evicted value from cache.
/// The outcome of an eviction from a cache.
///
/// Evictions occur in cache implementations on insert operations in a maxed out cache. This happens
/// to make space for the just inserted key/value pair.
#[derive(Debug, PartialEq, Eq)]
pub enum Eviction<K, V> {
/// Block eviction with evicted key/value pair on a key/value insertion with an unique key.
Block { key: K, value: V },

/// Value eviction on insertion with a key already existing in the cache.
Value(V),

/// No eviction when the cache is not maxed out.
None,
}

/// The outcome of a lookup query from a [`Cache`].
#[derive(Debug, PartialEq, Eq)]
pub enum Lookup<V> {
/// Cache hit.
Hit(V),

/// Cache miss.
Miss,
}

/// A size bounded map, where certain existing entries are evicted to make space for new entires.
/// A size bounded map, where certain existing entries are evicted to make space for new entries.
///
/// Implementations follow a well defined criteria to decide which cache blocks to evict in which
/// order. (e.g an LRU cache implementation would evict the least recently used cache blocks).
pub trait Cache<K, V> {
/// Associated error type.
type Error;

/// Inserts the given key/value pair into this cache.
fn insert(&mut self, key: K, value: V) -> Result<Eviction<K, V>, Self::Error>;

/// Removes the key/value pair associated with the given key from this cache.
fn remove(&mut self, key: &K) -> Result<Lookup<V>, Self::Error>;

/// Removes `(self.len() - new_capacity)` cache blocks to fit the new capacity. If the
/// difference is non-positive no cache blocks are removed.
fn shrink(&mut self, new_capacity: usize) -> Result<(), Self::Error>;

/// Reserves additional memory to accomodate the given number of additional cache blocks.
fn reserve(&mut self, additional: usize) -> Result<(), Self::Error>;

/// Queries this cache to find the value associated with given key.
fn query(&mut self, key: &K) -> Result<Lookup<&V>, Self::Error>;

/// Returns the current capacity of this cache.
fn capacity(&self) -> usize;

/// Returns the number of key/value pairs stored in this cache.
fn len(&self) -> usize;

/// Returns whether this cache is maxed out.
///
/// This method simply checks whether the current length of this cache equals its capacity.
fn is_maxed(&self) -> bool {
self.len() == self.capacity()
}

/// Returns whether this cache is empty.
fn is_empty(&self) -> bool {
self.len() == 0
}

/// Remove all items from this cache until it's empty.
fn clear(&mut self) -> Result<(), Self::Error>;
}

Expand Down
8 changes: 8 additions & 0 deletions src/collections/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,16 @@ pub struct LinkedList<V, T> {
len: usize,
}

/// Error type associated with list operations.
#[derive(Debug)]
pub enum ListError<VE> {
/// Used when there is an error in an operation performed on the underlying arena.
ArenaError(ArenaError<VE>),

/// Used when a link is not associated with a node in the underlying arena.
LinkBroken,

/// Used when attempting to remove items from an empty list.
ListEmpty,
}

Expand All @@ -71,6 +77,7 @@ where
}
}

/// Type alias for arena entries corresponding to [`LinkedList`] [`Node`] instances.
pub type LinkedListArenaEntry<T> = Entry<Node<T>>;

impl<V, T> LinkedList<V, T>
Expand Down Expand Up @@ -304,6 +311,7 @@ where
}
}

/// Iterator implementation to iterate over the items in a [`LinkedList`].
pub struct Iter<'a, V, T> {
list: &'a LinkedList<V, T>,
cursor: Option<&'a Link>,
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ pub mod map;
pub mod vector;

pub mod prelude {
//! The `generational-cache` prelude.
//!
//! This module provides a set of commonly used items to alleviate imports.

pub use super::{
arena::{Arena, ArenaError},
cache::{
Expand Down

0 comments on commit 6d9f568

Please sign in to comment.