Skip to main content

tenflowers_core/memory/
cache.rs

1//! Cache-friendly memory layout optimizations for improved performance
2//!
3//! This module provides cache optimization utilities including cache line alignment,
4//! blocked matrix layouts, and memory access pattern optimization.
5
6/// Cache line size detection and alignment utilities
7pub struct CacheOptimizer {
8    cache_line_size: usize,
9    l1_cache_size: usize,
10    l2_cache_size: usize,
11    tlb_page_size: usize,
12}
13
14impl CacheOptimizer {
15    /// Create new cache optimizer with detected parameters
16    pub fn new() -> Self {
17        Self {
18            cache_line_size: Self::detect_cache_line_size(),
19            l1_cache_size: Self::detect_l1_cache_size(),
20            l2_cache_size: Self::detect_l2_cache_size(),
21            tlb_page_size: Self::detect_page_size(),
22        }
23    }
24
25    /// Detect cache line size (typically 64 bytes on modern processors)
26    fn detect_cache_line_size() -> usize {
27        // Most modern processors use 64-byte cache lines
28        // ARM and x86-64 typically use 64 bytes
29        64
30    }
31
32    /// Detect L1 cache size (typically 32KB)
33    fn detect_l1_cache_size() -> usize {
34        32 * 1024 // 32KB typical L1 data cache
35    }
36
37    /// Detect L2 cache size (typically 256KB-512KB)
38    fn detect_l2_cache_size() -> usize {
39        256 * 1024 // 256KB typical L2 cache
40    }
41
42    /// Detect TLB page size (typically 4KB)
43    fn detect_page_size() -> usize {
44        4096 // 4KB pages on most systems
45    }
46
47    /// Get optimal alignment for data structures
48    pub fn get_optimal_alignment(&self, data_size: usize) -> usize {
49        if data_size <= self.cache_line_size {
50            // Align to cache line for small structures
51            self.cache_line_size
52        } else if data_size <= self.tlb_page_size {
53            // Align to page boundary for medium structures
54            self.tlb_page_size
55        } else {
56            // For large structures, align to multiple of page size
57            ((data_size + self.tlb_page_size - 1) / self.tlb_page_size) * self.tlb_page_size
58        }
59    }
60
61    /// Calculate optimal block size for cache-friendly iteration
62    pub fn get_optimal_block_size(&self, element_size: usize, total_elements: usize) -> usize {
63        let target_bytes = self.l1_cache_size / 2; // Use half of L1 for working set
64        let elements_per_block = target_bytes / element_size;
65
66        // Ensure block size is reasonable (not too small, not larger than total)
67        elements_per_block
68            .max(64) // Minimum block size
69            .min(total_elements) // Don't exceed total elements
70            .min(8192) // Maximum practical block size
71    }
72
73    /// Suggest memory access pattern optimization
74    pub fn optimize_access_pattern(&self, dims: &[usize], element_size: usize) -> AccessPattern {
75        let total_size = dims.iter().product::<usize>() * element_size;
76
77        if total_size <= self.l1_cache_size {
78            AccessPattern::Sequential // Fits in L1, sequential is best
79        } else if total_size <= self.l2_cache_size {
80            AccessPattern::Blocked {
81                block_size: self.get_optimal_block_size(element_size, dims[dims.len() - 1]),
82            }
83        } else {
84            AccessPattern::Tiled {
85                tile_dims: self.calculate_optimal_tile_size(dims, element_size),
86            }
87        }
88    }
89
90    /// Calculate optimal tile dimensions for large matrices
91    fn calculate_optimal_tile_size(&self, dims: &[usize], element_size: usize) -> Vec<usize> {
92        let target_bytes = self.l2_cache_size / 3; // Use 1/3 of L2 for each tile
93        let mut tile_dims = Vec::with_capacity(dims.len());
94
95        for &dim_size in dims {
96            let max_tile_elements = target_bytes / element_size;
97            let optimal_tile_size =
98                (max_tile_elements as f64).powf(1.0 / dims.len() as f64) as usize;
99            tile_dims.push(optimal_tile_size.min(dim_size).max(8)); // Min 8, max original dim
100        }
101
102        tile_dims
103    }
104
105    /// Get cache line size
106    pub fn cache_line_size(&self) -> usize {
107        self.cache_line_size
108    }
109
110    /// Get L1 cache size
111    pub fn l1_cache_size(&self) -> usize {
112        self.l1_cache_size
113    }
114
115    /// Get L2 cache size
116    pub fn l2_cache_size(&self) -> usize {
117        self.l2_cache_size
118    }
119
120    /// Get page size
121    pub fn page_size(&self) -> usize {
122        self.tlb_page_size
123    }
124}
125
126impl Default for CacheOptimizer {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132/// Memory access pattern recommendations
133#[derive(Debug, Clone)]
134pub enum AccessPattern {
135    Sequential,
136    Blocked { block_size: usize },
137    Tiled { tile_dims: Vec<usize> },
138}
139
140/// Cache-friendly matrix layout transformer
141pub struct MatrixLayoutOptimizer {
142    optimizer: CacheOptimizer,
143}
144
145impl MatrixLayoutOptimizer {
146    /// Create new matrix layout optimizer
147    pub fn new() -> Self {
148        Self {
149            optimizer: CacheOptimizer::new(),
150        }
151    }
152
153    /// Transform row-major matrix to cache-friendly blocked layout
154    pub fn to_blocked_layout<T: Copy>(
155        &self,
156        data: &[T],
157        rows: usize,
158        cols: usize,
159        block_size: usize,
160    ) -> Vec<T> {
161        let mut blocked_data = Vec::with_capacity(data.len());
162
163        // Iterate over blocks
164        for block_row in (0..rows).step_by(block_size) {
165            for block_col in (0..cols).step_by(block_size) {
166                // Process each block
167                for row in block_row..(block_row + block_size).min(rows) {
168                    for col in block_col..(block_col + block_size).min(cols) {
169                        let index = row * cols + col;
170                        blocked_data.push(data[index]);
171                    }
172                }
173            }
174        }
175
176        blocked_data
177    }
178
179    /// Transform blocked layout back to row-major
180    pub fn from_blocked_layout<T: Copy>(
181        &self,
182        blocked_data: &[T],
183        rows: usize,
184        cols: usize,
185        block_size: usize,
186    ) -> Vec<T> {
187        let mut data = vec![blocked_data[0]; blocked_data.len()]; // Initialize with first element
188        let mut blocked_idx = 0;
189
190        // Iterate over blocks in the same order as to_blocked_layout
191        for block_row in (0..rows).step_by(block_size) {
192            for block_col in (0..cols).step_by(block_size) {
193                // Process each element in the block
194                for row in block_row..(block_row + block_size).min(rows) {
195                    for col in block_col..(block_col + block_size).min(cols) {
196                        let index = row * cols + col;
197                        data[index] = blocked_data[blocked_idx];
198                        blocked_idx += 1;
199                    }
200                }
201            }
202        }
203
204        data
205    }
206
207    /// Suggest optimal matrix multiplication blocking parameters
208    pub fn suggest_gemm_blocking(
209        &self,
210        m: usize,
211        n: usize,
212        k: usize,
213        element_size: usize,
214    ) -> (usize, usize, usize) {
215        let target_bytes = self.optimizer.l2_cache_size / 3; // Divide L2 among A, B, C blocks
216        let target_elements = target_bytes / element_size;
217
218        // Calculate block sizes that fit in cache
219        let mk_block = (target_elements / n).max(8).min(m);
220        let nk_block = (target_elements / m).max(8).min(n);
221        let k_block = (target_elements / (mk_block + nk_block)).max(8).min(k);
222
223        (mk_block, nk_block, k_block)
224    }
225}
226
227impl Default for MatrixLayoutOptimizer {
228    fn default() -> Self {
229        Self::new()
230    }
231}
232
233/// Memory prefetching hints for improved cache performance
234pub struct PrefetchOptimizer {
235    prefetch_distance: usize,
236    stride_threshold: usize,
237}
238
239impl PrefetchOptimizer {
240    /// Create new prefetch optimizer
241    pub fn new() -> Self {
242        Self {
243            prefetch_distance: 64, // Prefetch 64 elements ahead
244            stride_threshold: 4,   // Use prefetching for strides >= 4
245        }
246    }
247
248    /// Add software prefetch hints for sequential access
249    pub fn prefetch_sequential<T>(&self, data: &[T], current_index: usize) {
250        let prefetch_index = current_index + self.prefetch_distance;
251        if prefetch_index < data.len() {
252            // Hint to prefetch data into cache
253            // In real implementation, this would use platform-specific intrinsics
254            // For now, we just touch the memory to simulate prefetching
255            let _prefetch_ptr = &data[prefetch_index] as *const T;
256        }
257    }
258
259    /// Add software prefetch hints for strided access
260    pub fn prefetch_strided<T>(&self, data: &[T], current_index: usize, stride: usize) {
261        if stride >= self.stride_threshold {
262            let prefetch_index = current_index + stride * self.prefetch_distance;
263            if prefetch_index < data.len() {
264                // Prefetch next element in strided pattern
265                let _prefetch_ptr = &data[prefetch_index] as *const T;
266            }
267        }
268    }
269
270    /// Suggest whether to use prefetching for given access pattern
271    pub fn should_prefetch(&self, access_stride: usize, data_size: usize) -> bool {
272        // Use prefetching for:
273        // 1. Large datasets (> 1MB)
274        // 2. Strided access patterns
275        // 3. When stride is large enough to benefit from prefetching
276        data_size > 1024 * 1024 || access_stride >= self.stride_threshold
277    }
278}
279
280impl Default for PrefetchOptimizer {
281    fn default() -> Self {
282        Self::new()
283    }
284}
285
286/// Global cache optimizer instance
287static GLOBAL_CACHE_OPTIMIZER: std::sync::OnceLock<CacheOptimizer> = std::sync::OnceLock::new();
288
289/// Get the global cache optimizer
290pub fn global_cache_optimizer() -> &'static CacheOptimizer {
291    GLOBAL_CACHE_OPTIMIZER.get_or_init(CacheOptimizer::new)
292}
293
294/// Utility function to align size to cache line boundary
295pub fn align_to_cache_line(size: usize) -> usize {
296    let cache_line_size = global_cache_optimizer().cache_line_size;
297    (size + cache_line_size - 1) & !(cache_line_size - 1)
298}
299
300/// Utility function to check if memory is cache-line aligned
301pub fn is_cache_aligned(ptr: *const u8) -> bool {
302    let cache_line_size = global_cache_optimizer().cache_line_size;
303    (ptr as usize) & (cache_line_size - 1) == 0
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn test_cache_optimizer_creation() {
312        let optimizer = CacheOptimizer::new();
313        assert_eq!(optimizer.cache_line_size(), 64);
314        assert_eq!(optimizer.l1_cache_size(), 32 * 1024);
315        assert_eq!(optimizer.l2_cache_size(), 256 * 1024);
316        assert_eq!(optimizer.page_size(), 4096);
317    }
318
319    #[test]
320    fn test_optimal_alignment() {
321        let optimizer = CacheOptimizer::new();
322
323        // Small data should align to cache line
324        assert_eq!(optimizer.get_optimal_alignment(32), 64);
325
326        // Medium data should align to page
327        assert_eq!(optimizer.get_optimal_alignment(1024), 4096);
328
329        // Large data should align to page multiple
330        let large_size = 8192;
331        let alignment = optimizer.get_optimal_alignment(large_size);
332        assert!(alignment >= large_size);
333        assert_eq!(alignment % 4096, 0);
334    }
335
336    #[test]
337    fn test_optimal_block_size() {
338        let optimizer = CacheOptimizer::new();
339
340        let block_size = optimizer.get_optimal_block_size(4, 1000);
341        assert!(block_size >= 64); // Minimum block size
342        assert!(block_size <= 1000); // Not larger than total
343        assert!(block_size <= 8192); // Maximum practical block size
344    }
345
346    #[test]
347    fn test_access_pattern_optimization() {
348        let optimizer = CacheOptimizer::new();
349
350        // Small data should use sequential access
351        let pattern = optimizer.optimize_access_pattern(&[100, 100], 4);
352        matches!(pattern, AccessPattern::Sequential);
353
354        // Large data should use tiled access
355        let pattern = optimizer.optimize_access_pattern(&[1000, 1000], 4);
356        matches!(pattern, AccessPattern::Tiled { .. });
357    }
358
359    #[test]
360    fn test_tile_size_calculation() {
361        let optimizer = CacheOptimizer::new();
362        let dims = vec![1000, 1000];
363        let tile_dims = optimizer.calculate_optimal_tile_size(&dims, 4);
364
365        assert_eq!(tile_dims.len(), 2);
366        for &tile_dim in &tile_dims {
367            assert!(tile_dim >= 8); // Minimum tile size
368            assert!(tile_dim <= 1000); // Not larger than original dimension
369        }
370    }
371
372    #[test]
373    fn test_matrix_layout_optimizer() {
374        let optimizer = MatrixLayoutOptimizer::new();
375
376        // Test blocking transformation
377        let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; // 3x4 matrix
378        let blocked = optimizer.to_blocked_layout(&data, 3, 4, 2);
379
380        // Should have same number of elements
381        assert_eq!(blocked.len(), data.len());
382
383        // Test reverse transformation
384        let restored = optimizer.from_blocked_layout(&blocked, 3, 4, 2);
385        assert_eq!(restored, data);
386    }
387
388    #[test]
389    fn test_gemm_blocking_suggestion() {
390        let optimizer = MatrixLayoutOptimizer::new();
391        let (mb, nb, kb) = optimizer.suggest_gemm_blocking(1000, 1000, 1000, 4);
392
393        // All block sizes should be reasonable
394        assert!(mb >= 8 && mb <= 1000);
395        assert!(nb >= 8 && nb <= 1000);
396        assert!(kb >= 8 && kb <= 1000);
397    }
398
399    #[test]
400    fn test_prefetch_optimizer() {
401        let optimizer = PrefetchOptimizer::new();
402
403        // Test prefetch decision
404        assert!(optimizer.should_prefetch(8, 2 * 1024 * 1024)); // Large data, large stride
405        assert!(!optimizer.should_prefetch(2, 1024)); // Small data, small stride
406    }
407
408    #[test]
409    fn test_cache_alignment_utilities() {
410        // Test alignment function
411        assert_eq!(align_to_cache_line(50), 64);
412        assert_eq!(align_to_cache_line(64), 64);
413        assert_eq!(align_to_cache_line(65), 128);
414
415        // Test alignment checking
416        let aligned_ptr = 0x1000 as *const u8; // 64-byte aligned
417        let unaligned_ptr = 0x1001 as *const u8; // Not aligned
418
419        assert!(is_cache_aligned(aligned_ptr));
420        assert!(!is_cache_aligned(unaligned_ptr));
421    }
422
423    #[test]
424    fn test_global_cache_optimizer() {
425        let optimizer1 = global_cache_optimizer();
426        let optimizer2 = global_cache_optimizer();
427
428        // Should be the same instance
429        assert!(std::ptr::eq(optimizer1, optimizer2));
430
431        // Should have expected values
432        assert_eq!(optimizer1.cache_line_size(), 64);
433    }
434
435    #[test]
436    fn test_access_patterns() {
437        let sequential = AccessPattern::Sequential;
438        matches!(sequential, AccessPattern::Sequential);
439
440        let blocked = AccessPattern::Blocked { block_size: 128 };
441        if let AccessPattern::Blocked { block_size } = blocked {
442            assert_eq!(block_size, 128);
443        }
444
445        let tiled = AccessPattern::Tiled {
446            tile_dims: vec![64, 64],
447        };
448        if let AccessPattern::Tiled { tile_dims } = tiled {
449            assert_eq!(tile_dims, vec![64, 64]);
450        }
451    }
452
453    #[test]
454    fn test_prefetch_methods() {
455        let optimizer = PrefetchOptimizer::new();
456        let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
457
458        // These methods don't return values, just test they don't panic
459        optimizer.prefetch_sequential(&data, 0);
460        optimizer.prefetch_strided(&data, 0, 2);
461
462        // Test edge cases
463        optimizer.prefetch_sequential(&data, data.len() - 1); // Near end
464        optimizer.prefetch_strided(&data, data.len() - 1, 1); // Near end with stride
465    }
466}