Skip to content

Commit

Permalink
adds LRU cache with lazy eviction
Browse files Browse the repository at this point in the history
The commit implements a variant of LRU cache with lazy eviction:
* Each entry maintains an associated ordinal value representing when the
  entry was last accessed.
* The cache is allowed to grow up to 2 times specified capacity with no
  evictions, at which point, the excess entries are evicted based on LRU
  policy resulting in an _amortized_ `O(1)` performance.

In many use cases which can allow the cache to store 2 times capacity
and can tolerate amortized nature of performance, this results in better
average performance as shown by the added benchmarks.

Additionally, with the existing implementation, `.get` requires a
mutable reference `&mut self`. In a multi-threaded setting, this
requires an exclusive write-lock on the cache even on the read path,
which can exacerbate lock contentions.
With lazy eviction, the ordinal values can be updated using atomic
operations, allowing shared lock for lookups.
  • Loading branch information
behzadnouri committed Nov 25, 2022
1 parent cf063f6 commit cb82a39
Show file tree
Hide file tree
Showing 4 changed files with 574 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,9 @@ nightly = ["hashbrown", "hashbrown/nightly"]
hashbrown = { version = "0.12", optional = true }

[dev-dependencies]
rand = "0.8.5"
scoped_threadpool = "0.1.*"
stats_alloc = "0.1.*"

[[bench]]
name = "benchmarks"
78 changes: 78 additions & 0 deletions benches/benchmarks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#![feature(test)]

extern crate core;
extern crate lru;
extern crate rand;
extern crate test;

use core::num::NonZeroUsize;
use rand::Rng;
use test::Bencher;

const REPS: usize = 1 << 10;
const NUM_KEYS: usize = 1 << 20;
const CAPACITY: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(1 << 17) };

macro_rules! impl_put_bench {
($name: ident, $cache: ty) => {
fn $name(bencher: &mut Bencher, capacity: NonZeroUsize, num_keys: usize, reps: usize) {
let mut rng = rand::thread_rng();
let mut cache = <$cache>::new(capacity);
for _ in 0..5 * capacity.get() {
let key = rng.gen_range(0..num_keys);
let _ = cache.put(key, ());
}
bencher.iter(|| {
for _ in 0..reps {
let key = rng.gen_range(0..num_keys);
let _ = cache.put(key, ());
}
});
}
};
}

macro_rules! impl_get_bench {
($name: ident, $cache: ty) => {
fn $name(bencher: &mut Bencher, capacity: NonZeroUsize, num_keys: usize, reps: usize) {
let mut rng = rand::thread_rng();
let mut cache = <$cache>::new(capacity);
for _ in 0..5 * capacity.get() {
let key = rng.gen_range(0..num_keys);
let _ = cache.put(key, ());
}
bencher.iter(|| {
for _ in 0..reps {
let key = rng.gen_range(0..num_keys);
let _ = cache.get(&key);
}
});
}
};
}

impl_put_bench!(run_put_bench, lru::LruCache<usize, ()>);
impl_put_bench!(run_put_bench_lazy, lru::lazy::LruCache<usize, ()>);

impl_get_bench!(run_get_bench, lru::LruCache<usize, ()>);
impl_get_bench!(run_get_bench_lazy, lru::lazy::LruCache<usize, ()>);

#[bench]
fn bench_put(bencher: &mut Bencher) {
run_put_bench(bencher, CAPACITY, NUM_KEYS, REPS);
}

#[bench]
fn bench_put_lazy(bencher: &mut Bencher) {
run_put_bench_lazy(bencher, CAPACITY, NUM_KEYS, REPS);
}

#[bench]
fn bench_get(bencher: &mut Bencher) {
run_get_bench(bencher, CAPACITY, NUM_KEYS, REPS);
}

#[bench]
fn bench_get_lazy(bencher: &mut Bencher) {
run_get_bench_lazy(bencher, CAPACITY, NUM_KEYS, REPS);
}
Loading

0 comments on commit cb82a39

Please sign in to comment.