A Simple, Efficient, and Scalable Eviction Algorithm
This project is a .NET implementation of SIEVE — a surprisingly effective yet simple cache eviction algorithm. It is designed to outperform LRU and other complex algorithms in real-world scenarios with minimal implementation overhead.
SIEVE stands for Simpler than LRU: an Efficient Turn-Key Eviction Algorithm. It combines lazy promotion with quick demotion, making it both efficient and easy to implement.
- 📉 Up to 63% lower miss ratio than ARC
- ⚡ Twice the throughput of optimized LRU at 16 threads
- 🔁 Lock-free hits for better concurrency
- 🧼 <20 lines of code change in most cache libraries
- 🔧 Can be used as a cache primitive to build more advanced eviction policies
Paper from Yazhuo Zhang, Juncheng Yang, Yao Yue, Ymir Vigfusson, K. V. Rashmi https://junchengyang.com/publication/nsdi24-SIEVE.pdf
https://cachemon.github.io/SIEVE-website/
public interface ICache<TKey, TValue>
where TKey : notnull
{
TValue? Get(TKey key);
void Put(TKey key, TValue value);
bool Contains(TKey key);
void Clear();
int Count { get; }
}
var cache = new SieveCache<string, string>(capacity: 3);
cache.Put("a", "apple");
cache.Put("b", "banana");
cache.Put("c", "coconut");
var result = cache.Get("a"); // "apple" – marks it as visited
cache.Put("d", "dragonfruit"); // evicts the first unvisited item
bool exists = cache.Contains("b"); // true or false depending on eviction
int currentCount = cache.Count;
- It is intended for single-threaded scenarios or environments where access is externally synchronized.
- Internally, SieveCache manages a linked structure (Next, Prev, Visited, etc.) without locking, for maximum performance.
- If you use it concurrently from multiple threads, you may encounter race conditions, such as:
- NullReferenceException
- corruption of the internal node list
- incorrect eviction behavior
Locking would significantly degrade performance — which contradicts the goals of SieveCache as presented in academic research papers, where minimal or no locking is a key advantage.
If you need thread safety:
- Use external synchronization (e.g. lock in your application)
- Or wrap SieveCache with your own concurrent-safe wrapper
- Or use a different cache strategy (e.g. MemoryCache, sharded/segment-based caches)
- Or implement lock mechanism on your own
Method | Capacity | AccessCount | Mean | Error | StdDev | Rank | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|---|---|---|---|
OptimizedSieveCache_RandomAccess | 100 | 1000 | 37.94 us | 1.509 us | 0.234 us | 1 | 1.0376 | 0.9766 | 0.9155 | 6.44 KB |
SieveCache_RandomAccess | 100 | 1000 | 36.69 us | 2.485 us | 0.645 us | 1 | 2.2583 | 0.0610 | - | 14.16 KB |
LruCache_RandomAccess | 100 | 1000 | 46.95 us | 3.923 us | 1.019 us | 1 | 3.4180 | 0.1221 | - | 21.19 KB |
OptimizedSieveCache_RandomAccess | 100 | 10000 | 383.27 us | 13.831 us | 3.592 us | 2 | 0.9766 | 0.4883 | - | 6.27 KB |
SieveCache_RandomAccess | 100 | 10000 | 392.33 us | 8.601 us | 1.331 us | 2 | 13.1836 | 0.4883 | - | 81.8 KB |
LruCache_RandomAccess | 100 | 10000 | 479.38 us | 21.386 us | 5.554 us | 2 | 16.1133 | 0.4883 | - | 101.41 KB |
OptimizedSieveCache_RandomAccess | 1000 | 1000 | 43.33 us | 1.218 us | 0.316 us | 1 | 5.5542 | 5.4932 | 0.0610 | 34.32 KB |
SieveCache_RandomAccess | 1000 | 1000 | 36.42 us | 3.951 us | 1.026 us | 1 | 8.4229 | 1.3428 | - | 52.11 KB |
LruCache_RandomAccess | 1000 | 1000 | 52.71 us | 1.616 us | 0.250 us | 1 | 7.0801 | 0.8545 | - | 43.63 KB |
OptimizedSieveCache_RandomAccess | 1000 | 10000 | 419.91 us | 18.669 us | 4.848 us | 2 | 5.3711 | 4.8828 | - | 34.34 KB |
SieveCache_RandomAccess | 1000 | 10000 | 422.31 us | 9.058 us | 2.352 us | 2 | 19.5313 | 4.8828 | - | 120.25 KB |
LruCache_RandomAccess | 1000 | 10000 | 515.71 us | 18.739 us | 4.866 us | 2 | 30.2734 | 10.7422 | - | 190.52 KB |