Skip to main content

mnemosyne_local/
fast_path_cache.rs

1//! Thread-local fast-path allocation caches for common size classes.
2//!
3//! This module implements per-thread fast-path caches that avoid contention on
4//! the global allocator for frequently-used allocation sizes (16, 32, 64, 128, 256 bytes).
5//! Each cache uses a simple bump-pointer pool with generation counters to enable
6//! efficient reuse.
7
8use mnemosyne_core::NUM_SIZE_CLASSES;
9
10/// Configuration for fast-path caches.
11pub struct FastPathCacheConfig {
12    /// Whether fast-path caching is enabled.
13    pub enabled: bool,
14    /// Maximum blocks to retain in each size class cache.
15    pub max_blocks_per_class: usize,
16    /// Size classes to cache (by index).
17    pub cached_size_classes: &'static [usize],
18}
19
20impl Default for FastPathCacheConfig {
21    fn default() -> Self {
22        Self {
23            enabled: true,
24            max_blocks_per_class: 256,
25            // Common sizes: 16, 32, 64, 128, 256 bytes (size class indices 0, 1, 3, 7, 11)
26            cached_size_classes: &[0, 1, 3, 7, 11],
27        }
28    }
29}
30
31/// A single slot in a fast-path cache for a specific size class.
32#[derive(Clone, Copy, Debug)]
33pub struct CacheBlock {
34    /// Pointer to the cached block.
35    pub ptr: *mut u8,
36    /// Generation counter for reuse tracking.
37    pub generation: u32,
38}
39
40impl CacheBlock {
41    /// Creates a new cache block.
42    #[inline]
43    pub fn new(ptr: *mut u8, generation: u32) -> Self {
44        Self { ptr, generation }
45    }
46
47    /// Returns true if this block is valid.
48    #[inline]
49    pub fn is_valid(&self) -> bool {
50        !self.ptr.is_null()
51    }
52}
53
54/// Per-size-class cache for frequently-used allocation sizes.
55#[derive(Clone, Copy, Debug)]
56pub struct SizeClassCache {
57    /// Bump pointer into the cache buffer.
58    pub bump: usize,
59    /// Total capacity of this cache.
60    pub capacity: usize,
61    /// Current generation counter (incremented on eviction).
62    pub generation: u32,
63    /// Number of hits for cache statistics.
64    pub hits: usize,
65    /// Number of misses for cache statistics.
66    pub misses: usize,
67}
68
69impl SizeClassCache {
70    /// Creates a new cache with the given capacity.
71    #[inline]
72    pub const fn new(capacity: usize) -> Self {
73        Self {
74            bump: 0,
75            capacity,
76            generation: 0,
77            hits: 0,
78            misses: 0,
79        }
80    }
81
82    /// Returns true if the cache has space available.
83    #[inline]
84    pub fn has_space(&self) -> bool {
85        self.bump < self.capacity
86    }
87
88    /// Attempts to allocate a block from the cache.
89    /// Returns the index if successful, None if cache is full.
90    #[inline]
91    pub fn allocate(&mut self) -> Option<usize> {
92        if self.has_space() {
93            let idx = self.bump;
94            self.bump += 1;
95            self.hits += 1;
96            Some(idx)
97        } else {
98            self.misses += 1;
99            None
100        }
101    }
102
103    /// Resets the cache by clearing the bump pointer and incrementing generation.
104    /// This is called when the cache fills up and needs to evict all entries.
105    #[inline]
106    pub fn reset(&mut self) {
107        self.bump = 0;
108        self.generation = self.generation.wrapping_add(1);
109    }
110
111    /// Returns the cache hit ratio as a percentage (0-100).
112    #[inline]
113    pub fn hit_ratio(&self) -> u8 {
114        let total = self.hits.saturating_add(self.misses);
115        if total == 0 {
116            0
117        } else {
118            ((self.hits as u128 * 100) / total as u128) as u8
119        }
120    }
121}
122
123/// Thread-local fast-path cache manager.
124///
125/// This structure manages multiple per-size-class caches, allowing
126/// threads to satisfy allocation requests from thread-local pools
127/// without contention on global allocator structures.
128pub struct FastPathCacheManager {
129    /// Per-size-class caches.
130    pub caches: [SizeClassCache; NUM_SIZE_CLASSES],
131    /// Configuration for this cache manager.
132    pub config: FastPathCacheConfig,
133    /// Total allocations served from fast-path.
134    pub fast_path_allocations: usize,
135    /// Total deallocations through fast-path.
136    pub fast_path_deallocations: usize,
137    /// Fallback allocations (slow path).
138    pub slow_path_allocations: usize,
139}
140
141impl FastPathCacheManager {
142    /// Creates a new fast-path cache manager with default configuration.
143    pub fn new() -> Self {
144        Self::with_config(FastPathCacheConfig::default())
145    }
146
147    /// Creates a new fast-path cache manager with a custom configuration.
148    pub fn with_config(config: FastPathCacheConfig) -> Self {
149        let mut caches: [SizeClassCache; NUM_SIZE_CLASSES] =
150            [SizeClassCache::new(0); NUM_SIZE_CLASSES];
151
152        if config.enabled {
153            for &size_class in config.cached_size_classes {
154                if size_class < NUM_SIZE_CLASSES {
155                    caches[size_class] = SizeClassCache::new(config.max_blocks_per_class);
156                }
157            }
158        }
159
160        Self {
161            caches,
162            config,
163            fast_path_allocations: 0,
164            fast_path_deallocations: 0,
165            slow_path_allocations: 0,
166        }
167    }
168
169    /// Attempts to allocate from the fast-path cache for the given size class.
170    /// Returns Some(index) if successful, None if the cache is disabled or full.
171    #[inline]
172    pub fn try_allocate(&mut self, size_class: usize) -> Option<usize> {
173        if !self.config.enabled || size_class >= NUM_SIZE_CLASSES {
174            return None;
175        }
176
177        // Check if this size class is configured for caching
178        if self.caches[size_class].capacity == 0 {
179            return None;
180        }
181
182        match self.caches[size_class].allocate() {
183            Some(idx) => {
184                self.fast_path_allocations += 1;
185                Some(idx)
186            }
187            None => {
188                self.slow_path_allocations += 1;
189                None
190            }
191        }
192    }
193
194    /// Records a deallocation in the fast-path cache.
195    #[inline]
196    pub fn record_deallocation(&mut self, size_class: usize) {
197        if size_class < NUM_SIZE_CLASSES {
198            self.fast_path_deallocations += 1;
199        }
200    }
201
202    /// Resets the cache for a specific size class.
203    #[inline]
204    pub fn reset_class_cache(&mut self, size_class: usize) {
205        if size_class < NUM_SIZE_CLASSES {
206            self.caches[size_class].reset();
207        }
208    }
209
210    /// Resets all caches.
211    pub fn reset_all(&mut self) {
212        for cache in &mut self.caches {
213            if cache.capacity > 0 {
214                cache.reset();
215            }
216        }
217    }
218
219    /// Returns cache statistics for a specific size class.
220    #[inline]
221    pub fn class_stats(&self, size_class: usize) -> Option<(usize, u8)> {
222        if size_class < NUM_SIZE_CLASSES {
223            let cache = self.caches[size_class];
224            Some((cache.hits.saturating_add(cache.misses), cache.hit_ratio()))
225        } else {
226            None
227        }
228    }
229
230    /// Returns overall fast-path cache efficiency metrics.
231    pub fn efficiency_metrics(&self) -> FastPathEfficiencyMetrics {
232        let total_requests = self
233            .fast_path_allocations
234            .saturating_add(self.slow_path_allocations);
235        let fast_path_ratio = if total_requests > 0 {
236            ((self.fast_path_allocations as u128 * 100) / total_requests as u128) as u8
237        } else {
238            0
239        };
240
241        let mut avg_cache_hit_ratio = 0u32;
242        let mut active_classes = 0usize;
243        for cache in &self.caches {
244            if cache.capacity > 0 {
245                avg_cache_hit_ratio += cache.hit_ratio() as u32;
246                active_classes += 1;
247            }
248        }
249        let avg_hit_ratio = if active_classes > 0 {
250            (avg_cache_hit_ratio / active_classes as u32) as u8
251        } else {
252            0
253        };
254
255        FastPathEfficiencyMetrics {
256            fast_path_allocations: self.fast_path_allocations,
257            slow_path_allocations: self.slow_path_allocations,
258            fast_path_ratio,
259            avg_cache_hit_ratio: avg_hit_ratio,
260            total_cache_accesses: total_requests,
261        }
262    }
263}
264
265impl Default for FastPathCacheManager {
266    fn default() -> Self {
267        Self::new()
268    }
269}
270
271/// Snapshot of fast-path cache efficiency metrics.
272#[derive(Clone, Copy, Debug)]
273pub struct FastPathEfficiencyMetrics {
274    /// Number of allocations served from fast-path caches.
275    pub fast_path_allocations: usize,
276    /// Number of allocations requiring slow path (miss or disabled).
277    pub slow_path_allocations: usize,
278    /// Ratio of fast-path allocations as a percentage (0-100).
279    pub fast_path_ratio: u8,
280    /// Average cache hit ratio across active size classes (0-100).
281    pub avg_cache_hit_ratio: u8,
282    /// Total cache accesses (hits + misses).
283    pub total_cache_accesses: usize,
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_cache_block_validity() {
292        let ptr = 0x1000 as *mut u8;
293        let block = CacheBlock::new(ptr, 0);
294        assert!(block.is_valid());
295
296        let null_block = CacheBlock::new(core::ptr::null_mut(), 0);
297        assert!(!null_block.is_valid());
298    }
299
300    #[test]
301    fn test_size_class_cache_allocation() {
302        let mut cache = SizeClassCache::new(10);
303        assert!(cache.has_space());
304        assert_eq!(cache.allocate(), Some(0));
305        assert_eq!(cache.allocate(), Some(1));
306        assert_eq!(cache.hits, 2);
307    }
308
309    #[test]
310    fn test_size_class_cache_full() {
311        let mut cache = SizeClassCache::new(2);
312        assert_eq!(cache.allocate(), Some(0));
313        assert_eq!(cache.allocate(), Some(1));
314        assert_eq!(cache.allocate(), None);
315        assert_eq!(cache.misses, 1);
316    }
317
318    #[test]
319    fn test_size_class_cache_hit_ratio() {
320        let mut cache = SizeClassCache::new(100);
321        for _ in 0..80 {
322            let _ = cache.allocate();
323        }
324        cache.misses = 20;
325        assert_eq!(cache.hit_ratio(), 80);
326    }
327
328    #[test]
329    fn test_fast_path_cache_manager() {
330        let mut manager = FastPathCacheManager::new();
331
332        // Size class 0 (16 bytes) should be cached
333        assert!(manager.try_allocate(0).is_some());
334        assert_eq!(manager.fast_path_allocations, 1);
335
336        // Size class 2 (48 bytes) is not in default cached list
337        assert!(manager.try_allocate(2).is_none());
338    }
339
340    #[test]
341    fn test_efficiency_metrics() {
342        let mut manager = FastPathCacheManager::new();
343
344        let _ = manager.try_allocate(0);
345        let _ = manager.try_allocate(0);
346
347        let metrics = manager.efficiency_metrics();
348        assert_eq!(metrics.fast_path_allocations, 2);
349        assert_eq!(metrics.total_cache_accesses, 2);
350    }
351}