| Crates.io | genswap |
| lib.rs | genswap |
| version | 0.1.0 |
| created_at | 2026-01-18 19:53:42.944855+00 |
| updated_at | 2026-01-18 19:53:42.944855+00 |
| description | Generation-tracked ArcSwap wrapper for 5-1600x faster cached reads in read-heavy workloads |
| homepage | https://github.com/temporalxyz/genswap |
| repository | https://github.com/temporalxyz/genswap |
| max_upload_size | |
| id | 2052969 |
| size | 90,284 |
A high-performance Rust library providing generation-tracked caching around ArcSwap for read-heavy workloads. Achieve 5-1500x faster reads compared to ArcSwap::load_full() by eliminating atomic refcount operations on cache hits.
In read-heavy scenarios where shared data is updated infrequently, repeatedly calling ArcSwap::load_full() incurs unnecessary overhead from atomic refcount operations. This library solves that by:
The result: cache hits only perform a single atomic load and comparison, avoiding all refcount operations.
See BENCHMARK_RESULTS.md for detailed analysis.
Single-threaded performance:
ArcSwap::load_full() on cache hitsload_full()Multi-threaded performance (8 threads):
ArcSwap::load_full()load_full()Cache hit rate impact:
Add this to your Cargo.toml:
[dependencies]
genswap = "0.1.0"
use arc_generation_cache::GenSwap;
use std::sync::Arc;
use std::thread;
// Create shared config
let config = Arc::new(GenSwap::new(42u64));
// Spawn reader threads
let handles: Vec<_> = (0..4).map(|_| {
let config = Arc::clone(&config);
thread::spawn(move || {
// Each thread gets its own cached reader
let mut reader = config.reader();
for _ in 0..1_000_000 {
let value = reader.get(); // Fast! Only checks generation
// Use value...
}
})
}).collect();
// Update from another thread (infrequent)
config.update(100);
for h in handles {
h.join().unwrap();
}
use arc_generation_cache::GenSwap;
use std::sync::Arc;
#[derive(Clone)]
struct AppConfig {
feature_flags: Vec<String>,
rate_limit: u32,
timeout_ms: u64,
}
fn main() {
let config = Arc::new(GenSwap::new(AppConfig {
feature_flags: vec!["feature_a".into()],
rate_limit: 100,
timeout_ms: 1000,
}));
// In your request handler (called millions of times):
let mut reader = config.reader();
loop {
// This is incredibly fast - just an atomic load + comparison
let cfg = reader.get();
if cfg.rate_limit > 0 {
// Process request with current config
}
// Config updates are automatically detected and loaded
}
}
GenSwap<T>The producer-side type that holds the data and generation counter.
// Create
let swap = GenSwap::new(data);
let swap = GenSwap::new_from_arc(arc);
// Update
swap.update(new_data); // Increments generation
swap.update_arc(Arc::new(new_data));
// Read-copy-update
swap.rcu(|current| modify(current)); // Atomic update based on current value
// Direct access (for one-off reads)
let arc = swap.load_full();
let guard = swap.load();
// Create cached readers
let mut reader = swap.reader();
// Query
let gen = swap.generation();
CachedReader<T>The consumer-side handle that caches an Arc<T> locally.
let mut reader = swap.reader();
// Primary method - fast path on cache hit
let data: &Arc<T> = reader.get();
// Check if stale without reloading
if reader.is_stale() {
println!("Data has been updated");
}
// Get owned Arc
let arc = reader.get_clone();
// Force reload even if generation matches
reader.force_refresh();
// Query
let gen = reader.cached_generation();
let cached_ref = reader.cached(); // Without staleness check
Ideal for:
Not ideal for:
ArcSwap directly)The implementation uses carefully chosen memory orderings for correctness:
// Producer: Release ordering ensures data is visible before generation
self.data.store(new_arc);
self.generation.fetch_add(1, Ordering::Release);
// Consumer: Acquire ordering pairs with Release
let current_gen = source.generation.load(Ordering::Acquire);
if current_gen != cached_generation {
// Generation changed, reload data
}
This guarantees that when a reader observes a new generation, the corresponding data update is visible.
GenSwap<T> is Send + Sync where T: Send + SyncCachedReader<T> is Send but NOT SyncCachedReader instanceGenSwapArc<GenSwap<T>>, so no lifetime constraintsThread 1: load_full() → atomic_fetch_add(refcount) → use data → atomic_fetch_sub(refcount)
Thread 2: load_full() → atomic_fetch_add(refcount) → use data → atomic_fetch_sub(refcount)
...
Problem: Every read requires two atomic operations (increment + decrement) on a shared refcount, causing cache line contention.
Setup (once per thread):
reader = swap.reader() → stores Arc<T> + generation
Hot path (millions of times):
reader.get() → load(generation) → compare → return cached Arc
↑
Only this atomic load, no refcount operations!
Update (rare):
swap.update(new_data) → store(data) → fetch_add(generation)
Solution: Cached readers only check a generation counter (single atomic load). The cached Arc's refcount doesn't change, avoiding contention.
Run benchmarks with:
cargo bench
See BENCHMARK_RESULTS.md for detailed analysis.
| Approach | Single-thread | Multi-thread (8 cores) | Notes |
|---|---|---|---|
Arc::clone() |
3.88 ns | High contention | Direct clone, high overhead |
ArcSwap::load() |
3.03 ns | Moderate contention | Returns Guard, no refcount |
ArcSwap::load_full() |
4.20 ns | 619 ns | Returns Arc, refcount ops |
CachedReader::get() |
0.72 ns | 0.40 ns | Best performance |
See the examples/ directory:
# Basic usage example
cargo run --example basic_usage
# Run all tests
cargo test
# Run with output
cargo test -- --nocapture
# Run specific test
cargo test test_cached_reader_cache_hit
Generation counter wrapping: The u64 generation counter will wrap after 2^64 updates. In practice, this is not a concern (would take millions of years at 1M updates/sec).
Memory overhead: Each CachedReader stores an Arc<T> and a u64, adding ~16-24 bytes per reader.
Eventual consistency: Readers may observe stale data for a brief period between updates. They're guaranteed to see the update on their next get() call.
Not Clone: CachedReader is intentionally not Clone to prevent cache aliasing. Each reader should be independent.
Contributions are welcome! Please feel free to submit a Pull Request.
Licensed under either of:
at your option.
Built on top of the excellent arc-swap crate by Michal 'vorner' Vaner.
Inspired by the need for high-performance shared configuration in read-heavy systems.
arc-swap - Atomic Arc swap operationsleft-right - Alternative read-optimized concurrency primitiveevmap - Eventually consistent, lock-free map