SIEVE cache
A high-performance implementation of the SIEVE cache replacement algorithm for Rust, with both single-threaded and thread-safe variants.
Overview
SIEVE is an eviction algorithm that is simpler than LRU but achieves state-of-the-art efficiency on skewed workloads. It works by maintaining a single bit per entry that tracks whether an item has been "visited" since it was last considered for eviction.
Key Features
- Simple and efficient: SIEVE requires less state than LRU or LFU algorithms
- Good performance on skewed workloads: Particularly effective for real-world access patterns
- Adaptive capacity management: Recommendation system to optimize cache size based on utilization
- Multiple implementations:
SieveCache: Core single-threaded implementationSyncSieveCache: Thread-safe wrapper using a single lockShardedSieveCache: High-concurrency implementation using multiple locks
This implementation exposes the same API as the clock-pro and arc-cache crates, so it can be used as a drop-in replacement for them in existing applications.
Basic Usage
The library provides three cache implementations with different threading characteristics.
SieveCache - Single-Threaded Implementation
The core SieveCache implementation is designed for single-threaded use. It's the most efficient option when thread safety isn't required.
use SieveCache;
// Create a new cache with a specific capacity
let mut cache: = new.unwrap;
// Insert key/value pairs into the cache
cache.insert;
cache.insert;
// Retrieve a value from the cache (returns a reference to the value)
assert_eq!;
// Check if a key exists in the cache
assert!;
assert!;
// Get a mutable reference to a value
if let Some = cache.get_mut
// Remove an entry from the cache
let removed_value = cache.remove;
assert_eq!;
// Query cache information
assert_eq!; // Number of entries
assert_eq!; // Maximum capacity
assert!; // Check if empty
// Manual eviction (normally handled automatically when capacity is reached)
let evicted = cache.evict; // Returns and removes a value that wasn't recently accessed
// Get a recommendation for optimal cache capacity based on current utilization
let recommended = cache.recommended_capacity;
println!;
Thread-Safe Implementations
These implementations are available when using the appropriate feature flags:
SyncSieveCacheis available with thesyncfeature (enabled by default)ShardedSieveCacheis available with theshardedfeature (enabled by default)
SyncSieveCache - Basic Thread-Safe Cache
For concurrent access from multiple threads, you can use the SyncSieveCache wrapper, which provides thread safety with a single global lock:
use SyncSieveCache;
use thread;
// Create a thread-safe cache
let cache = new.unwrap;
// The cache can be safely cloned and shared between threads
let cache_clone = cache.clone;
// Insert from the main thread
cache.insert;
// Access from another thread
let handle = spawn;
// Wait for the thread to complete
handle.join.unwrap;
// Check if keys exist
assert!;
assert!;
// Remove an entry
let removed = cache.remove;
assert_eq!;
// Perform multiple operations atomically with exclusive access
cache.with_lock;
Key differences from the non-thread-safe version:
- Methods take
&selfinstead of&mut self get()returns a clone of the value instead of a referencewith_lock()method provides atomic multi-operation transactions
ShardedSieveCache - High-Performance Thread-Safe Cache
For applications with high concurrency requirements, the ShardedSieveCache implementation uses multiple internal locks (sharding) to reduce contention and improve throughput:
use ShardedSieveCache;
use thread;
use Arc;
// Create a sharded cache with default shard count (16)
// We use Arc for sharing between threads
let cache = new;
// Alternatively, specify a custom number of shards
// let cache = Arc::new(ShardedSieveCache::with_shards(100000, 32).unwrap());
// Insert data from the main thread
cache.insert;
// Use multiple worker threads to insert data concurrently
let mut handles = vec!;
for i in 0..8
// Wait for all threads to complete
for handle in handles
// Shard-specific atomic operations
// with_key_lock locks only the shard containing the key
cache.with_key_lock;
// Get the number of entries across all shards
// Note: This acquires all shard locks sequentially
let total_entries = cache.len;
assert_eq!; // 800 from threads + 1 "foo" + 2 related keys
// Access shard information
println!;
How Sharding Works
The ShardedSieveCache divides the cache into multiple independent segments (shards), each protected by its own mutex. When an operation is performed on a key:
- The key is hashed to determine which shard it belongs to
- Only that shard's lock is acquired for the operation
- Operations on keys in different shards can proceed in parallel
This design significantly reduces lock contention when operations are distributed across different keys, making it ideal for high-concurrency workloads.
Feature Flags
This crate provides the following feature flags to control which implementations are available:
sync: Enables the thread-safeSyncSieveCacheimplementation (enabled by default)sharded: Enables the shardedShardedSieveCacheimplementation (enabled by default)
If you only need specific implementations, you can select just the features you need:
# Only use the core implementation
= { = "1", = false }
# Only use the core and sync implementations
= { = "1", = false, = ["sync"] }
# Only use the core and sharded implementations
= { = "1", = false, = ["sharded"] }
# For documentation tests to work correctly
= { = "1", = ["doctest"] }
Adaptive Cache Sizing
The library includes a mechanism to recommend optimal cache sizes based on current utilization patterns.
How it Works
The recommended_capacity function analyzes:
- The ratio of "visited" entries (recently accessed) to total entries
- How full the cache is relative to its capacity
- User-defined thresholds for scaling decisions
Based on this analysis, it recommends:
- Increasing capacity when many entries are frequently accessed (high utilization)
- Decreasing capacity when few entries are frequently accessed (low utilization)
- Maintaining current capacity when utilization is within normal parameters
Usage
use SieveCache;
let mut cache = new.unwrap;
// Add and access items...
// (usage pattern will affect the recommendation)
// Get recommended capacity with custom parameters
let recommended = cache.recommended_capacity;
println!;
println!;
// Optionally resize your cache based on the recommendation
// (requires creating a new cache with the recommended size)
if recommended != cache.capacity
This adaptive sizing capability is available in all three cache implementations:
// Thread-safe version (with "sync" feature enabled)
// use sieve_cache::SyncSieveCache;
// let cache = SyncSieveCache::<String, u32>::new(1000).unwrap();
// let recommended = cache.recommended_capacity(0.5, 2.0, 0.3, 0.7);
// Sharded high-concurrency version (with "sharded" feature enabled)
// use sieve_cache::ShardedSieveCache;
// let cache = ShardedSieveCache::<String, u32>::new(1000).unwrap();
// let recommended = cache.recommended_capacity(0.5, 2.0, 0.3, 0.7);
Performance Considerations
Choosing the right cache implementation depends on your workload:
- Single-threaded usage: Use
SieveCache- it's the most efficient with the lowest overhead - Moderate concurrency: Use
SyncSieveCache- simple and effective with moderate thread count - High concurrency: Use
ShardedSieveCache- best performance with many threads accessing different keys- Sharding is most effective when operations are distributed across many keys
- If most operations target the same few keys (which map to the same shards), the benefits may be limited
- Generally, 16-32 shards provide a good balance of concurrency and overhead for most applications