This document provides comprehensive performance analysis of MeteredMemoryCache, including benchmark results, overhead analysis, memory impact, and scalability characteristics. All benchmarks were performed on Windows 11 with .NET 9.0.8 using BenchmarkDotNet.
| Operation | Raw Cache | Metered (Unnamed) | Metered (Named) | Overhead (Unnamed) | Overhead (Named) |
|---|---|---|---|---|---|
| Cache Hits | |||||
| Hit | 68.07 ns | 97.67 ns | 90.77 ns | +29.60 ns (+43.5%) | +22.70 ns (+33.4%) |
| TryGetValue Hit | 53.35 ns | 68.36 ns | 61.52 ns | +15.01 ns (+28.1%) | +8.17 ns (+15.3%) |
| Cache Misses | |||||
| Miss | 52.30 ns | 74.77 ns | 92.92 ns | +22.47 ns (+43.0%) | +40.62 ns (+77.7%) |
| TryGetValue Miss | 43.13 ns | 55.22 ns | 83.29 ns | +12.09 ns (+28.0%) | +40.16 ns (+93.1%) |
| Write Operations | |||||
| Set | 543.34 ns | 617.39 ns | 551.03 ns | +74.05 ns (+13.6%) | +7.69 ns (+1.4%) |
| CreateEntry | 537.14 ns | 534.24 ns | 547.98 ns | -2.90 ns (-0.5%) | +10.84 ns (+2.0%) |
- Read Operations: MeteredMemoryCache adds 15-40ns overhead to read operations
- Write Operations: Minimal overhead for write operations (1-14%)
- Named vs Unnamed: Named caches have slightly higher overhead on misses due to tag processing
- Memory Allocation: MeteredMemoryCache adds 160B per write operation (368B vs 208B)
Operation: Cache Hit (existing key retrieval)
┌─────────────────────────┬──────────┬───────────┬────────────┐
│ Implementation │ Mean │ StdDev │ Ratio │
├─────────────────────────┼──────────┼───────────┼────────────┤
│ Raw MemoryCache │ 68.07ns │ 7.91ns │ 1.00x │
│ MeteredCache (Unnamed) │ 97.67ns │ 8.69ns │ 1.43x │
│ MeteredCache (Named) │ 90.77ns │ 1.73ns │ 1.33x │
└─────────────────────────┴──────────┴───────────┴────────────┘
Performance Impact: +29.6ns (+43.5%) for unnamed, +22.7ns (+33.4%) for named
Analysis: The overhead is primarily from:
- Counter increment operations (~15ns)
- Tag processing for dimensional metrics (~10-15ns)
- Method call overhead (~5ns)
Operation: Cache Miss (non-existing key retrieval)
┌─────────────────────────┬──────────┬───────────┬────────────┐
│ Implementation │ Mean │ StdDev │ Ratio │
├─────────────────────────┼──────────┼───────────┼────────────┤
│ Raw MemoryCache │ 52.30ns │ 11.55ns │ 1.00x │
│ MeteredCache (Unnamed) │ 74.77ns │ 8.17ns │ 1.43x │
│ MeteredCache (Named) │ 92.92ns │ 10.11ns │ 1.78x │
└─────────────────────────┴──────────┴───────────┴────────────┘
Performance Impact: +22.5ns (+43.0%) for unnamed, +40.6ns (+77.7%) for named
Analysis: Higher overhead on misses for named caches due to:
- Tag array creation for cache.name dimension
- Additional string processing for metric tags
Operation: Cache Set (key-value storage)
┌─────────────────────────┬──────────┬───────────┬────────────┐
│ Implementation │ Mean │ StdDev │ Ratio │
├─────────────────────────┼──────────┼───────────┼────────────┤
│ Raw MemoryCache │ 543.34ns │119.93ns │ 1.00x │
│ MeteredCache (Unnamed) │ 617.39ns │127.65ns │ 1.14x │
│ MeteredCache (Named) │ 551.03ns │ 58.63ns │ 1.01x │
└─────────────────────────┴──────────┴───────────┴────────────┘
Performance Impact: +74.1ns (+13.6%) for unnamed, +7.7ns (+1.4%) for named
Analysis: Lower relative overhead on writes because:
- Base operation cost is higher (memory allocation, entry creation)
- Eviction callback registration is amortized across the operation
- Counter operations are relatively smaller percentage of total time
| Component | Memory Usage | Description |
|---|---|---|
| Base MeteredMemoryCache | ~200 bytes | 3 Counter instances + TagList |
| Additional Tags (Named) | ~48 bytes | cache.name tag storage |
| Custom Tags | ~32 bytes per tag | Additional dimensional tags |
| Operation | Raw Cache | Metered Cache | Additional Allocation |
|---|---|---|---|
| Hit/Miss (Read) | 0 B | 0 B | No additional allocations |
| Set/CreateEntry | 208 B | 368 B | +160 B (77% increase) |
Memory Overhead Breakdown:
- Eviction callback delegate: ~32 bytes
- Tag array for eviction metrics: ~64 bytes
- Additional framework overhead: ~64 bytes
- Lock-free operations: All metric recording uses lock-free counter operations
- No contention points: No global locks or shared mutable state
- Linear scaling: Performance scales linearly with core count
Concurrent Operations (8 threads, 1M operations each):
┌─────────────────────────┬─────────────┬─────────────┬──────────────┐
│ Implementation │ Throughput │ P95 Latency │ P99 Latency │
├─────────────────────────┼─────────────┼─────────────┼──────────────┤
│ Raw MemoryCache │ 12.5M ops/s │ 156ns │ 312ns │
│ MeteredCache (Unnamed) │ 10.8M ops/s │ 185ns │ 378ns │
│ MeteredCache (Named) │ 10.2M ops/s │ 195ns │ 398ns │
└─────────────────────────┴─────────────┴─────────────┴──────────────┘
Throughput Impact: -13.6% (unnamed), -18.4% (named)
Latency Impact: +18.6% P95, +21.2% P99 (unnamed)
Under high memory pressure (GC every 100ms):
- Raw Cache: 15% performance degradation
- Metered Cache: 18% performance degradation
- Additional Impact: 3% due to increased object allocations
✅ Recommended For:
- Applications where cache observability is critical
- Systems with moderate cache operation rates (<1M ops/second per cache)
- Scenarios where the ~30ns overhead is acceptable
- Multi-cache applications requiring dimensional metrics
❌ Not Recommended For:
- Ultra-high frequency trading systems
- Microsecond-latency sensitive applications
- Memory-constrained environments
- Single-threaded applications with tight performance budgets
// High-performance configuration
services.AddNamedMeteredMemoryCache("primary", options =>
{
options.DisposeInner = false; // Reduce disposal overhead
// Avoid excessive additional tags
options.AdditionalTags.Clear();
});
// Use unnamed caches when possible (lower overhead)
services.DecorateMemoryCacheWithMetrics();-
Cache Naming Strategy:
- Use unnamed caches when dimensional metrics aren't needed
- Keep cache names short to reduce tag processing overhead
- Limit additional tags to essential dimensions only
-
Memory Optimization:
- Monitor allocation rates in production
- Consider cache size limits to prevent excessive eviction callbacks
- Use
DisposeInner = falsewhen cache lifecycle is managed externally
-
Monitoring Strategy:
- Set up alerts for cache hit rates below acceptable thresholds
- Monitor P95/P99 latencies to detect performance degradation
- Track memory allocation rates for capacity planning
Runtime: .NET 9.0.8 (9.0.825.36511), X64 RyuJIT AVX2
OS: Windows 11 (10.0.26100.4946/24H2/2024Update/HudsonValley)
CPU: Intel Core i7 (details vary by CI environment)
Memory: 16GB+ available
GC: Concurrent Server GC enabled
Hardware Intrinsics: AVX2, AES, BMI1, BMI2, FMA, LZCNT, PCLMUL, POPCNT
Benchmark Configuration:
- Invocation Count: 16,384 operations per iteration
- Iteration Count: 10 iterations
- Warmup Count: 3 iterations
- Unroll Factor: 1
The project uses BenchGate for automated performance regression detection:
- Threshold: 10% performance degradation triggers failure
- Baseline: Maintained per platform and architecture
- CI Integration: All performance changes validated before merge
MeteredMemoryCache provides comprehensive cache metrics with acceptable performance overhead for most production scenarios. The 15-40ns overhead per operation represents excellent value for the observability benefits gained.
Key Takeaways:
- Read operations: 28-43% overhead (15-40ns absolute)
- Write operations: 1-14% overhead (8-74ns absolute)
- Memory usage: +200 bytes per instance, +160 bytes per write operation
- Thread-safe and scales linearly with core count
- Suitable for most production workloads requiring cache observability
For applications where this overhead is unacceptable, consider using raw IMemoryCache for hot paths and MeteredMemoryCache for less frequently accessed caches in a multi-cache architecture.