Skip to main content

sklears_decomposition/
performance.rs

1//! Performance Optimization Utilities
2//!
3//! This module provides performance optimization techniques including:
4//! - Cache-friendly matrix layouts and operations
5//! - Memory-aligned data structures
6//! - Prefetching for large matrix operations
7//! - SIMD-friendly data arrangements
8//! - Memory pooling and arena allocation
9
10use scirs2_core::ndarray::Array2;
11use sklears_core::{
12    error::{Result, SklearsError},
13    types::Float,
14};
15use std::alloc::{alloc, dealloc, Layout};
16use std::ptr;
17
18/// Cache line size for modern CPUs (typically 64 bytes)
19#[allow(dead_code)]
20const CACHE_LINE_SIZE: usize = 64;
21
22/// Alignment for SIMD operations (32 bytes for AVX)
23#[allow(dead_code)]
24const SIMD_ALIGNMENT: usize = 32;
25
26/// Cache-friendly matrix configuration
27#[derive(Debug, Clone)]
28pub struct CacheConfig {
29    /// Tile size for matrix operations
30    pub tile_size: usize,
31
32    /// Prefetch distance
33    pub prefetch_distance: usize,
34
35    /// Use memory alignment
36    pub use_alignment: bool,
37
38    /// Block size for blocked algorithms
39    pub block_size: usize,
40}
41
42impl Default for CacheConfig {
43    fn default() -> Self {
44        Self {
45            tile_size: 64,
46            prefetch_distance: 8,
47            use_alignment: true,
48            block_size: 128,
49        }
50    }
51}
52
53/// Cache-friendly matrix wrapper
54pub struct CacheFriendlyMatrix {
55    data: Vec<Float>,
56    nrows: usize,
57    ncols: usize,
58    layout: MatrixLayout,
59    config: CacheConfig,
60}
61
62/// Matrix storage layout
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum MatrixLayout {
65    /// Row-major layout (C-style)
66    RowMajor,
67
68    /// Column-major layout (Fortran-style)
69    ColumnMajor,
70
71    /// Blocked/tiled layout for cache efficiency
72    Blocked,
73}
74
75impl CacheFriendlyMatrix {
76    /// Create new cache-friendly matrix from Array2
77    pub fn new(array: Array2<Float>, layout: MatrixLayout, config: CacheConfig) -> Self {
78        let (nrows, ncols) = array.dim();
79
80        let data = match layout {
81            MatrixLayout::RowMajor => array.into_raw_vec_and_offset().0,
82            MatrixLayout::ColumnMajor => {
83                // Transpose to column-major
84                let mut data = Vec::with_capacity(nrows * ncols);
85                for j in 0..ncols {
86                    for i in 0..nrows {
87                        data.push(array[[i, j]]);
88                    }
89                }
90                data
91            }
92            MatrixLayout::Blocked => {
93                // Block layout for cache efficiency
94                Self::convert_to_blocked(&array, config.block_size)
95            }
96        };
97
98        Self {
99            data,
100            nrows,
101            ncols,
102            layout,
103            config,
104        }
105    }
106
107    /// Convert to blocked layout
108    fn convert_to_blocked(array: &Array2<Float>, block_size: usize) -> Vec<Float> {
109        let (nrows, ncols) = array.dim();
110        let mut blocked = Vec::with_capacity(nrows * ncols);
111
112        let n_row_blocks = nrows.div_ceil(block_size);
113        let n_col_blocks = ncols.div_ceil(block_size);
114
115        for block_i in 0..n_row_blocks {
116            for block_j in 0..n_col_blocks {
117                let row_start = block_i * block_size;
118                let row_end = (row_start + block_size).min(nrows);
119                let col_start = block_j * block_size;
120                let col_end = (col_start + block_size).min(ncols);
121
122                for i in row_start..row_end {
123                    for j in col_start..col_end {
124                        blocked.push(array[[i, j]]);
125                    }
126                }
127
128                // Pad to full block size
129                let block_elements = (row_end - row_start) * (col_end - col_start);
130                let full_block = block_size * block_size;
131                if full_block > block_elements {
132                    blocked.resize(blocked.len() + (full_block - block_elements), 0.0);
133                }
134            }
135        }
136
137        blocked
138    }
139
140    /// Get element at position
141    pub fn get(&self, i: usize, j: usize) -> Float {
142        match self.layout {
143            MatrixLayout::RowMajor => self.data[i * self.ncols + j],
144            MatrixLayout::ColumnMajor => self.data[j * self.nrows + i],
145            MatrixLayout::Blocked => {
146                let block_size = self.config.block_size;
147                let block_i = i / block_size;
148                let block_j = j / block_size;
149                let local_i = i % block_size;
150                let local_j = j % block_size;
151
152                let n_col_blocks = self.ncols.div_ceil(block_size);
153                let block_idx = block_i * n_col_blocks + block_j;
154                let block_start = block_idx * block_size * block_size;
155                let offset = local_i * block_size + local_j;
156
157                self.data[block_start + offset]
158            }
159        }
160    }
161
162    /// Convert back to Array2
163    pub fn to_array(&self) -> Array2<Float> {
164        let mut array = Array2::zeros((self.nrows, self.ncols));
165
166        for i in 0..self.nrows {
167            for j in 0..self.ncols {
168                array[[i, j]] = self.get(i, j);
169            }
170        }
171
172        array
173    }
174
175    /// Cache-friendly matrix multiplication
176    pub fn matmul(&self, other: &Self) -> Result<Self> {
177        if self.ncols != other.nrows {
178            return Err(SklearsError::InvalidInput(format!(
179                "Dimension mismatch: {} x {} cannot multiply {} x {}",
180                self.nrows, self.ncols, other.nrows, other.ncols
181            )));
182        }
183
184        let mut result_data = vec![0.0; self.nrows * other.ncols];
185
186        // Tiled matrix multiplication for cache efficiency
187        let tile_size = self.config.tile_size;
188
189        for i_tile in (0..self.nrows).step_by(tile_size) {
190            for j_tile in (0..other.ncols).step_by(tile_size) {
191                for k_tile in (0..self.ncols).step_by(tile_size) {
192                    // Process tile
193                    let i_end = (i_tile + tile_size).min(self.nrows);
194                    let j_end = (j_tile + tile_size).min(other.ncols);
195                    let k_end = (k_tile + tile_size).min(self.ncols);
196
197                    for i in i_tile..i_end {
198                        for j in j_tile..j_end {
199                            let mut sum = 0.0;
200                            for k in k_tile..k_end {
201                                sum += self.get(i, k) * other.get(k, j);
202                            }
203                            let idx = i * other.ncols + j;
204                            result_data[idx] += sum;
205                        }
206                    }
207                }
208            }
209        }
210
211        Ok(Self {
212            data: result_data,
213            nrows: self.nrows,
214            ncols: other.ncols,
215            layout: MatrixLayout::RowMajor,
216            config: self.config.clone(),
217        })
218    }
219
220    /// Get matrix dimensions
221    pub fn dim(&self) -> (usize, usize) {
222        (self.nrows, self.ncols)
223    }
224}
225
226/// Memory pool for matrix allocations
227pub struct MemoryPool {
228    /// Pool of available buffers
229    buffers: Vec<Vec<Float>>,
230
231    /// Buffer size
232    buffer_size: usize,
233
234    /// Maximum pool size
235    max_pool_size: usize,
236}
237
238impl MemoryPool {
239    /// Create new memory pool
240    pub fn new(buffer_size: usize, max_pool_size: usize) -> Self {
241        Self {
242            buffers: Vec::new(),
243            buffer_size,
244            max_pool_size,
245        }
246    }
247
248    /// Acquire buffer from pool
249    pub fn acquire(&mut self) -> Vec<Float> {
250        self.buffers
251            .pop()
252            .unwrap_or_else(|| Vec::with_capacity(self.buffer_size))
253    }
254
255    /// Return buffer to pool
256    pub fn release(&mut self, mut buffer: Vec<Float>) {
257        if self.buffers.len() < self.max_pool_size {
258            buffer.clear();
259            self.buffers.push(buffer);
260        }
261        // If pool is full, buffer is dropped
262    }
263
264    /// Clear pool
265    pub fn clear(&mut self) {
266        self.buffers.clear();
267    }
268
269    /// Get pool statistics
270    pub fn stats(&self) -> PoolStats {
271        PoolStats {
272            available_buffers: self.buffers.len(),
273            buffer_size: self.buffer_size,
274            total_allocated_bytes: self.buffers.len()
275                * self.buffer_size
276                * std::mem::size_of::<Float>(),
277        }
278    }
279}
280
281/// Memory pool statistics
282#[derive(Debug, Clone)]
283pub struct PoolStats {
284    pub available_buffers: usize,
285    pub buffer_size: usize,
286    pub total_allocated_bytes: usize,
287}
288
289/// Aligned memory allocator for SIMD operations
290pub struct AlignedAllocator;
291
292impl AlignedAllocator {
293    /// Allocate aligned memory
294    pub fn allocate(size: usize, alignment: usize) -> Result<*mut Float> {
295        if size == 0 {
296            return Ok(ptr::null_mut());
297        }
298
299        unsafe {
300            let layout = Layout::from_size_align(size * std::mem::size_of::<Float>(), alignment)
301                .map_err(|e| SklearsError::InvalidInput(format!("Invalid layout: {}", e)))?;
302
303            let ptr = alloc(layout) as *mut Float;
304
305            if ptr.is_null() {
306                return Err(SklearsError::InvalidInput("Allocation failed".to_string()));
307            }
308
309            Ok(ptr)
310        }
311    }
312
313    /// Deallocate aligned memory
314    ///
315    /// # Safety
316    ///
317    /// `ptr` must have been allocated by `AlignedMemory::allocate` with the same `size` and
318    /// `alignment`, and must not have been freed already.
319    pub unsafe fn deallocate(ptr: *mut Float, size: usize, alignment: usize) {
320        if ptr.is_null() || size == 0 {
321            return;
322        }
323
324        let layout =
325            Layout::from_size_align_unchecked(size * std::mem::size_of::<Float>(), alignment);
326
327        dealloc(ptr as *mut u8, layout);
328    }
329
330    /// Create a zero-initialized, custom-aligned buffer.
331    ///
332    /// This intentionally returns [`crate::hardware_acceleration::AlignedBuffer`]
333    /// rather than `Vec<Float>`: `Vec::from_raw_parts` requires the backing
334    /// allocation to have been made with exactly `Layout::array::<Float>(cap)`
335    /// (i.e. `align_of::<Float>()`), since that is the layout `Vec`'s `Drop`
336    /// uses on deallocation. Handing a `Vec` a pointer allocated with a
337    /// *different* (e.g. SIMD/cache-line) alignment -- which is the entire
338    /// point of an "aligned" allocator -- would deallocate with a mismatched
339    /// `Layout` and is undefined behavior. `AlignedBuffer` stores its own
340    /// `Layout` and frees with the exact layout it was allocated with, so it
341    /// is sound for any requested alignment.
342    pub fn aligned_vec(
343        size: usize,
344        alignment: usize,
345    ) -> Result<crate::hardware_acceleration::AlignedBuffer> {
346        Ok(crate::hardware_acceleration::AlignedBuffer::new(
347            size, alignment,
348        ))
349    }
350}
351
352/// Prefetch hint for cache optimization
353#[inline(always)]
354pub fn prefetch_hint<T>(_addr: *const T) {
355    #[cfg(target_arch = "x86_64")]
356    {
357        unsafe {
358            #[cfg(target_feature = "sse")]
359            {
360                std::arch::x86_64::_mm_prefetch::<{ std::arch::x86_64::_MM_HINT_T0 }>(
361                    _addr as *const i8,
362                );
363            }
364        }
365    }
366    // For other architectures, this is a no-op
367}
368
369/// Performance profiler for decomposition operations
370pub struct PerformanceProfiler {
371    measurements: Vec<Measurement>,
372}
373
374#[derive(Debug, Clone)]
375pub struct Measurement {
376    pub name: String,
377    pub duration_ns: u128,
378    pub memory_bytes: usize,
379}
380
381impl PerformanceProfiler {
382    /// Create new profiler
383    pub fn new() -> Self {
384        Self {
385            measurements: Vec::new(),
386        }
387    }
388
389    /// Start timing an operation
390    pub fn start(&mut self, name: &str) -> TimingGuard {
391        TimingGuard {
392            name: name.to_string(),
393            start: std::time::Instant::now(),
394        }
395    }
396
397    /// Record measurement
398    pub fn record(&mut self, name: String, duration_ns: u128, memory_bytes: usize) {
399        self.measurements.push(Measurement {
400            name,
401            duration_ns,
402            memory_bytes,
403        });
404    }
405
406    /// Get measurements
407    pub fn measurements(&self) -> &[Measurement] {
408        &self.measurements
409    }
410
411    /// Clear measurements
412    pub fn clear(&mut self) {
413        self.measurements.clear();
414    }
415
416    /// Generate report
417    pub fn report(&self) -> String {
418        let mut report = String::from("Performance Report:\n");
419        report.push_str("===================\n\n");
420
421        for m in &self.measurements {
422            report.push_str(&format!(
423                "{}: {:.2} ms, {} bytes\n",
424                m.name,
425                m.duration_ns as f64 / 1_000_000.0,
426                m.memory_bytes
427            ));
428        }
429
430        let total_time: u128 = self.measurements.iter().map(|m| m.duration_ns).sum();
431        let total_memory: usize = self.measurements.iter().map(|m| m.memory_bytes).sum();
432
433        report.push_str(&format!(
434            "\nTotal: {:.2} ms, {} bytes\n",
435            total_time as f64 / 1_000_000.0,
436            total_memory
437        ));
438
439        report
440    }
441}
442
443impl Default for PerformanceProfiler {
444    fn default() -> Self {
445        Self::new()
446    }
447}
448
449/// Timing guard for automatic measurement
450pub struct TimingGuard {
451    name: String,
452    start: std::time::Instant,
453}
454
455impl TimingGuard {
456    /// Finish timing and return duration
457    pub fn finish(self) -> (String, u128) {
458        let duration = self.start.elapsed().as_nanos();
459        (self.name, duration)
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    #[test]
468    fn test_cache_friendly_matrix_creation() {
469        let array =
470            Array2::from_shape_vec((3, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
471                .expect("operation should succeed");
472
473        let cache_matrix = CacheFriendlyMatrix::new(
474            array.clone(),
475            MatrixLayout::RowMajor,
476            CacheConfig::default(),
477        );
478
479        assert_eq!(cache_matrix.dim(), (3, 3));
480        assert_eq!(cache_matrix.get(0, 0), 1.0);
481        assert_eq!(cache_matrix.get(2, 2), 9.0);
482    }
483
484    #[test]
485    fn test_cache_friendly_matmul() {
486        let a = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
487            .expect("shape and data length should match");
488        let b = Array2::from_shape_vec((3, 2), vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0])
489            .expect("shape and data length should match");
490
491        let cache_a = CacheFriendlyMatrix::new(a, MatrixLayout::RowMajor, CacheConfig::default());
492        let cache_b = CacheFriendlyMatrix::new(b, MatrixLayout::RowMajor, CacheConfig::default());
493
494        let result = cache_a.matmul(&cache_b).expect("operation should succeed");
495        assert_eq!(result.dim(), (2, 2));
496
497        // Expected result: [[58, 64], [139, 154]]
498        assert!((result.get(0, 0) - 58.0).abs() < 1e-6);
499        assert!((result.get(0, 1) - 64.0).abs() < 1e-6);
500    }
501
502    #[test]
503    fn test_memory_pool() {
504        let mut pool = MemoryPool::new(100, 5);
505
506        // Acquire and release buffers
507        let buf1 = pool.acquire();
508        let buf2 = pool.acquire();
509
510        pool.release(buf1);
511        pool.release(buf2);
512
513        let stats = pool.stats();
514        assert_eq!(stats.available_buffers, 2);
515
516        // Acquire again should reuse buffer
517        let _buf3 = pool.acquire();
518        let stats = pool.stats();
519        assert_eq!(stats.available_buffers, 1);
520    }
521
522    #[test]
523    fn test_performance_profiler() {
524        let mut profiler = PerformanceProfiler::new();
525
526        let guard = profiler.start("test_operation");
527        std::thread::sleep(std::time::Duration::from_millis(10));
528        let (name, duration) = guard.finish();
529
530        profiler.record(name, duration, 1024);
531
532        let measurements = profiler.measurements();
533        assert_eq!(measurements.len(), 1);
534        assert!(measurements[0].duration_ns > 0);
535    }
536
537    #[test]
538    fn test_column_major_layout() {
539        let array = Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0])
540            .expect("shape and data length should match");
541
542        let cache_matrix = CacheFriendlyMatrix::new(
543            array.clone(),
544            MatrixLayout::ColumnMajor,
545            CacheConfig::default(),
546        );
547
548        assert_eq!(cache_matrix.get(0, 0), 1.0);
549        assert_eq!(cache_matrix.get(0, 1), 2.0);
550        assert_eq!(cache_matrix.get(1, 0), 3.0);
551        assert_eq!(cache_matrix.get(1, 1), 4.0);
552    }
553
554    #[test]
555    fn test_profiler_report() {
556        let mut profiler = PerformanceProfiler::new();
557
558        profiler.record("op1".to_string(), 1_000_000, 1024);
559        profiler.record("op2".to_string(), 2_000_000, 2048);
560
561        let report = profiler.report();
562        assert!(report.contains("op1"));
563        assert!(report.contains("op2"));
564        assert!(report.contains("Total"));
565    }
566}