ruvector_core/
cache_optimized.rs

1//! Cache-optimized data structures using Structure-of-Arrays (SoA) layout
2//!
3//! This module provides cache-friendly layouts for vector storage to minimize
4//! cache misses and improve memory access patterns.
5
6use std::alloc::{alloc, dealloc, Layout};
7use std::ptr;
8
9/// Cache line size (typically 64 bytes on modern CPUs)
10const CACHE_LINE_SIZE: usize = 64;
11
12/// Structure-of-Arrays layout for vectors
13///
14/// Instead of storing vectors as Vec<Vec<f32>>, we store all components
15/// separately to improve cache locality during SIMD operations.
16#[repr(align(64))] // Align to cache line boundary
17pub struct SoAVectorStorage {
18    /// Number of vectors
19    count: usize,
20    /// Dimensions per vector
21    dimensions: usize,
22    /// Capacity (allocated vectors)
23    capacity: usize,
24    /// Storage for each dimension separately
25    /// Layout: [dim0_vec0, dim0_vec1, ..., dim0_vecN, dim1_vec0, ...]
26    data: *mut f32,
27}
28
29impl SoAVectorStorage {
30    /// Maximum allowed dimensions to prevent overflow
31    const MAX_DIMENSIONS: usize = 65536;
32    /// Maximum allowed capacity to prevent overflow
33    const MAX_CAPACITY: usize = 1 << 24; // ~16M vectors
34
35    /// Create a new SoA vector storage
36    ///
37    /// # Panics
38    /// Panics if dimensions or capacity exceed safe limits or would cause overflow.
39    pub fn new(dimensions: usize, initial_capacity: usize) -> Self {
40        // Security: Validate inputs to prevent integer overflow
41        assert!(
42            dimensions > 0 && dimensions <= Self::MAX_DIMENSIONS,
43            "dimensions must be between 1 and {}",
44            Self::MAX_DIMENSIONS
45        );
46        assert!(
47            initial_capacity <= Self::MAX_CAPACITY,
48            "initial_capacity exceeds maximum of {}",
49            Self::MAX_CAPACITY
50        );
51
52        let capacity = initial_capacity.next_power_of_two();
53
54        // Security: Use checked arithmetic to prevent overflow
55        let total_elements = dimensions
56            .checked_mul(capacity)
57            .expect("dimensions * capacity overflow");
58        let total_bytes = total_elements
59            .checked_mul(std::mem::size_of::<f32>())
60            .expect("total size overflow");
61
62        let layout =
63            Layout::from_size_align(total_bytes, CACHE_LINE_SIZE).expect("invalid memory layout");
64
65        let data = unsafe { alloc(layout) as *mut f32 };
66
67        // Zero initialize
68        unsafe {
69            ptr::write_bytes(data, 0, total_elements);
70        }
71
72        Self {
73            count: 0,
74            dimensions,
75            capacity,
76            data,
77        }
78    }
79
80    /// Add a vector to the storage
81    pub fn push(&mut self, vector: &[f32]) {
82        assert_eq!(vector.len(), self.dimensions);
83
84        if self.count >= self.capacity {
85            self.grow();
86        }
87
88        // Store each dimension separately
89        for (dim_idx, &value) in vector.iter().enumerate() {
90            let offset = dim_idx * self.capacity + self.count;
91            unsafe {
92                *self.data.add(offset) = value;
93            }
94        }
95
96        self.count += 1;
97    }
98
99    /// Get a vector by index (copies to output buffer)
100    pub fn get(&self, index: usize, output: &mut [f32]) {
101        assert!(index < self.count);
102        assert_eq!(output.len(), self.dimensions);
103
104        for dim_idx in 0..self.dimensions {
105            let offset = dim_idx * self.capacity + index;
106            output[dim_idx] = unsafe { *self.data.add(offset) };
107        }
108    }
109
110    /// Get a slice of a specific dimension across all vectors
111    /// This allows efficient SIMD operations on a single dimension
112    pub fn dimension_slice(&self, dim_idx: usize) -> &[f32] {
113        assert!(dim_idx < self.dimensions);
114        let offset = dim_idx * self.capacity;
115        unsafe { std::slice::from_raw_parts(self.data.add(offset), self.count) }
116    }
117
118    /// Get a mutable slice of a specific dimension
119    pub fn dimension_slice_mut(&mut self, dim_idx: usize) -> &mut [f32] {
120        assert!(dim_idx < self.dimensions);
121        let offset = dim_idx * self.capacity;
122        unsafe { std::slice::from_raw_parts_mut(self.data.add(offset), self.count) }
123    }
124
125    /// Number of vectors stored
126    pub fn len(&self) -> usize {
127        self.count
128    }
129
130    /// Check if empty
131    pub fn is_empty(&self) -> bool {
132        self.count == 0
133    }
134
135    /// Dimensions per vector
136    pub fn dimensions(&self) -> usize {
137        self.dimensions
138    }
139
140    /// Grow the storage capacity
141    fn grow(&mut self) {
142        let new_capacity = self.capacity * 2;
143
144        // Security: Use checked arithmetic to prevent overflow
145        let new_total_elements = self
146            .dimensions
147            .checked_mul(new_capacity)
148            .expect("dimensions * new_capacity overflow");
149        let new_total_bytes = new_total_elements
150            .checked_mul(std::mem::size_of::<f32>())
151            .expect("total size overflow in grow");
152
153        let new_layout = Layout::from_size_align(new_total_bytes, CACHE_LINE_SIZE)
154            .expect("invalid memory layout in grow");
155
156        let new_data = unsafe { alloc(new_layout) as *mut f32 };
157
158        // Copy old data dimension by dimension
159        for dim_idx in 0..self.dimensions {
160            let old_offset = dim_idx * self.capacity;
161            let new_offset = dim_idx * new_capacity;
162
163            unsafe {
164                ptr::copy_nonoverlapping(
165                    self.data.add(old_offset),
166                    new_data.add(new_offset),
167                    self.count,
168                );
169            }
170        }
171
172        // Deallocate old data
173        let old_layout = Layout::from_size_align(
174            self.dimensions * self.capacity * std::mem::size_of::<f32>(),
175            CACHE_LINE_SIZE,
176        )
177        .unwrap();
178
179        unsafe {
180            dealloc(self.data as *mut u8, old_layout);
181        }
182
183        self.data = new_data;
184        self.capacity = new_capacity;
185    }
186
187    /// Compute distance from query to all stored vectors using dimension-wise operations
188    /// This takes advantage of the SoA layout for better cache utilization
189    pub fn batch_euclidean_distances(&self, query: &[f32], output: &mut [f32]) {
190        assert_eq!(query.len(), self.dimensions);
191        assert_eq!(output.len(), self.count);
192
193        // Initialize output with zeros
194        output.fill(0.0);
195
196        // Process dimension by dimension
197        for dim_idx in 0..self.dimensions {
198            let dim_slice = self.dimension_slice(dim_idx);
199            let query_val = query[dim_idx];
200
201            // Compute squared differences for this dimension
202            for vec_idx in 0..self.count {
203                let diff = dim_slice[vec_idx] - query_val;
204                output[vec_idx] += diff * diff;
205            }
206        }
207
208        // Take square root
209        for distance in output.iter_mut() {
210            *distance = distance.sqrt();
211        }
212    }
213}
214
215impl Drop for SoAVectorStorage {
216    fn drop(&mut self) {
217        let layout = Layout::from_size_align(
218            self.dimensions * self.capacity * std::mem::size_of::<f32>(),
219            CACHE_LINE_SIZE,
220        )
221        .unwrap();
222
223        unsafe {
224            dealloc(self.data as *mut u8, layout);
225        }
226    }
227}
228
229unsafe impl Send for SoAVectorStorage {}
230unsafe impl Sync for SoAVectorStorage {}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_soa_storage() {
238        let mut storage = SoAVectorStorage::new(3, 4);
239
240        storage.push(&[1.0, 2.0, 3.0]);
241        storage.push(&[4.0, 5.0, 6.0]);
242
243        assert_eq!(storage.len(), 2);
244
245        let mut output = vec![0.0; 3];
246        storage.get(0, &mut output);
247        assert_eq!(output, vec![1.0, 2.0, 3.0]);
248
249        storage.get(1, &mut output);
250        assert_eq!(output, vec![4.0, 5.0, 6.0]);
251    }
252
253    #[test]
254    fn test_dimension_slice() {
255        let mut storage = SoAVectorStorage::new(3, 4);
256
257        storage.push(&[1.0, 2.0, 3.0]);
258        storage.push(&[4.0, 5.0, 6.0]);
259        storage.push(&[7.0, 8.0, 9.0]);
260
261        // Get all values for dimension 0
262        let dim0 = storage.dimension_slice(0);
263        assert_eq!(dim0, &[1.0, 4.0, 7.0]);
264
265        // Get all values for dimension 1
266        let dim1 = storage.dimension_slice(1);
267        assert_eq!(dim1, &[2.0, 5.0, 8.0]);
268    }
269
270    #[test]
271    fn test_batch_distances() {
272        let mut storage = SoAVectorStorage::new(3, 4);
273
274        storage.push(&[1.0, 0.0, 0.0]);
275        storage.push(&[0.0, 1.0, 0.0]);
276        storage.push(&[0.0, 0.0, 1.0]);
277
278        let query = vec![1.0, 0.0, 0.0];
279        let mut distances = vec![0.0; 3];
280
281        storage.batch_euclidean_distances(&query, &mut distances);
282
283        assert!((distances[0] - 0.0).abs() < 0.001);
284        assert!((distances[1] - 1.414).abs() < 0.01);
285        assert!((distances[2] - 1.414).abs() < 0.01);
286    }
287}