Lock Contention (Arc<RwLock<HashMap>>)
get_value() acquires write lockString Allocations
term.to_string() on every accessString keys (24 bytes + heap allocation)Arc Cloning Overhead
Arc::clone() on every node transition during traversalInstant::now() System Calls
get_value() (once for lookup, once for update)No Batching or Coalescing
std::sync::RwLock with parking_lot::RwLockBenefits:
Implementation:
// Add to Cargo.toml
[dependencies]
parking_lot = "0.12"
// In eviction wrapper files:
use parking_lot::RwLock;
Estimated improvement: 15-25% faster for single-threaded, 2-3x for multi-threaded
Tradeoffs: None significant (parking_lot is well-tested)
Arc<str> instead of String for HashMap keysBenefits:
&strImplementation:
use std::collections::HashMap;
use std::sync::Arc;
type MetadataMap = HashMap<Arc<str>, EntryMetadata>;
// Lookup with borrowed str
fn lookup(&self, term: &str) -> Option<...> {
self.metadata.read().get(term).cloned()
}
// Insert with Arc<str>
fn insert(&self, term: &str) {
let key: Arc<str> = Arc::from(term);
self.metadata.write().insert(key, ...);
}
Estimated improvement: 5-10% fewer allocations
Tradeoffs: Slightly more complex key management
HashMap with DashMapBenefits:
Implementation:
// Add to Cargo.toml
[dependencies]
dashmap = "6.1"
// In wrapper:
use dashmap::DashMap;
use std::sync::Arc;
pub struct Lru<D> {
inner: D,
metadata: Arc<DashMap<Arc<str>, EntryMetadata>>,
}
impl<D> Lru<D> {
fn record_access(&self, term: &str) {
self.metadata
.entry(Arc::from(term))
.and_modify(|m| m.update_access())
.or_insert_with(EntryMetadata::new);
}
}
Estimated improvement: 3-10x faster for 4+ concurrent threads
Tradeoffs:
batch_record_access() for bulk operationsBenefits:
Implementation:
pub fn batch_record_access(&self, terms: &[&str]) {
let mut metadata = self.metadata.write();
for &term in terms {
metadata.entry(Arc::from(term))
.and_modify(|m| m.update_access())
.or_insert_with(EntryMetadata::new);
}
}
Estimated improvement: 50-80% faster for batch updates
Option A: Coarse-grained timestamps
use std::sync::atomic::{AtomicU64, Ordering};
static CURRENT_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
// Background thread updates every 100ms
fn timestamp_updater() {
loop {
let now = Instant::now().elapsed().as_millis() as u64;
CURRENT_TIMESTAMP.store(now, Ordering::Relaxed);
thread::sleep(Duration::from_millis(100));
}
}
// In metadata:
struct EntryMetadata {
last_accessed: u64, // milliseconds since epoch
}
fn update_access(&mut self) {
self.last_accessed = CURRENT_TIMESTAMP.load(Ordering::Relaxed);
}
Option B: Lazy timestamp updates
// Only update timestamp if > 1 second old
fn update_access(&mut self) {
let now = Instant::now();
if now.duration_since(self.last_accessed) > Duration::from_secs(1) {
self.last_accessed = now;
}
}
Estimated improvement: 10-20% fewer system calls
Tradeoffs: Slightly less precise timestamps (usually acceptable for caching)
Current size per entry:
String key: 24 bytes + heap allocationInstant: 16 bytes (two u64s)Optimized size:
Arc<str> key: 16 bytes (shared)u64 timestamp: 8 bytesu32 hit_count: 4 bytes (for LFU)u32 size: 4 bytes (for memory pressure)Implementation:
#[repr(C)]
struct CompactMetadata {
last_accessed_ms: u64,
hit_count: u32,
size_bytes: u32,
}
Add Cargo feature flags for different optimization profiles:
[features]
default = ["eviction-opt-balanced"]
# Optimization profiles
eviction-opt-none = [] # No optimizations, std lib only
eviction-opt-balanced = [ # Good balance (default)
"eviction-parking-lot",
"eviction-compact-metadata"
]
eviction-opt-concurrent = [ # Max concurrent performance
"eviction-dashmap",
"eviction-parking-lot",
"eviction-compact-metadata",
"eviction-coarse-timestamps"
]
eviction-opt-memory = [ # Min memory usage
"eviction-compact-metadata"
]
# Individual optimizations
eviction-parking-lot = ["parking_lot"]
eviction-dashmap = ["dashmap"]
eviction-compact-metadata = []
eviction-coarse-timestamps = []
For simple counters (LFU), use AtomicU32:
use std::sync::atomic::{AtomicU32, Ordering};
struct LfuMetadata {
access_count: AtomicU32,
}
impl LfuMetadata {
fn increment(&self) {
self.access_count.fetch_add(1, Ordering::Relaxed);
}
}
Estimated improvement: Near-zero contention for LFU
For finding min/max scores across candidates:
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
unsafe fn find_max_score_simd(scores: &[f64]) -> usize {
// Use AVX2 for parallel max reduction
// Process 4 f64s at a time
}
use rand::random;
fn maybe_record_access(&self, term: &str) {
// Only record 50% of accesses
if random::<u8>() < 128 {
self.record_access(term);
}
}
Estimated improvement: 50% fewer metadata updates
Tradeoffs: Less precise LRU/LFU tracking
std::sync::RwLock with parking_lot::RwLockString to Arc<str>| Optimization | Single-thread | Multi-thread (4) | Memory |
|---|---|---|---|
| Baseline | 1.0x | 1.0x | 100% |
| + parking_lot | 1.2x | 2.5x | 95% |
| + Arc | 1.3x | 2.5x | 85% |
| + DashMap | 1.3x | 8.0x | 90% |
| + Compact | 1.4x | 8.0x | 70% |
| All | 1.5x | 10x | 65% |
| Optimization | Breaking Change | MSRV Impact | Dependencies |
|---|---|---|---|
| parking_lot | No | None | +1 (parking_lot) |
| Arc | No (internal) | None | 0 |
| DashMap | No (behind flag) | None | +1 (dashmap) |
| Compact metadata | No (internal) | None | 0 |
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |