Skip to main content

velesdb_core/
contiguous_ops.rs

1//! Reorder, batch distance, and lifecycle operations for `ContiguousVectors`.
2//!
3//! Extracted from `perf_optimizations.rs` to reduce NLOC.
4//! Contains reorder permutation, dot-product batching, Drop, and free SIMD helpers.
5
6use super::perf_optimizations::ContiguousVectors;
7use std::alloc::dealloc;
8use std::ptr::{self, NonNull};
9
10// =============================================================================
11// ContiguousVectors: Reorder + Dot-Product + Drop
12// =============================================================================
13
14impl ContiguousVectors {
15    /// Reorders vectors according to the given permutation.
16    ///
17    /// `new_order[i]` contains the old index of the vector that should occupy
18    /// position `i` after reordering. The permutation must have exactly
19    /// `self.len()` elements and every index must be `< self.len()`.
20    ///
21    /// # Errors
22    ///
23    /// Returns an error if:
24    /// - `new_order.len() != self.len()`
25    /// - Any index in `new_order` is out of bounds
26    /// - The new buffer allocation fails
27    pub fn reorder(&mut self, new_order: &[usize]) -> crate::error::Result<()> {
28        if new_order.len() != self.count {
29            return Err(crate::error::Error::Internal(format!(
30                "Reorder permutation length {} != vector count {}",
31                new_order.len(),
32                self.count
33            )));
34        }
35        if self.count == 0 {
36            return Ok(());
37        }
38
39        self.reorder_copy(new_order)
40    }
41
42    /// Performs the out-of-place vector copy for reordering.
43    ///
44    /// Allocates a temporary buffer, copies vectors in permuted order, then
45    /// swaps the buffer into place. Uses `AllocGuard` for panic-safety.
46    fn reorder_copy(&mut self, new_order: &[usize]) -> crate::error::Result<()> {
47        use crate::alloc_guard::AllocGuard;
48
49        let new_layout = Self::layout(self.dimension, self.count)?;
50        let guard = AllocGuard::new_zeroed(new_layout).ok_or_else(|| {
51            crate::error::Error::AllocationFailed(format!(
52                "Reorder: failed to allocate {} bytes",
53                new_layout.size()
54            ))
55        })?;
56        let new_ptr = NonNull::new(guard.cast::<f32>()).ok_or_else(|| {
57            crate::error::Error::AllocationFailed(
58                "Reorder: AllocGuard returned null pointer".to_string(),
59            )
60        })?;
61
62        self.copy_permuted_vectors(new_ptr.as_ptr(), new_order)?;
63
64        // Transfer ownership — guard will not free on drop
65        let _ = guard.into_raw();
66
67        // Deallocate old buffer
68        let old_layout = Self::layout(self.dimension, self.capacity)?;
69        // SAFETY: self.data was allocated with old_layout, is non-null (NonNull invariant).
70        // - Condition 1: old_layout matches the allocation parameters.
71        // - Condition 2: Pointer is non-null per NonNull invariant.
72        // SAFETY: Free old buffer after data migration to reordered buffer.
73        unsafe { dealloc(self.data.as_ptr().cast::<u8>(), old_layout) };
74
75        self.data = new_ptr;
76        self.capacity = self.count;
77        Ok(())
78    }
79
80    /// Copies vectors from the current buffer to `dst` in permuted order.
81    fn copy_permuted_vectors(
82        &self,
83        dst: *mut f32,
84        new_order: &[usize],
85    ) -> crate::error::Result<()> {
86        let dim = self.dimension;
87        for (new_idx, &old_idx) in new_order.iter().enumerate() {
88            if old_idx >= self.count {
89                return Err(crate::error::Error::Internal(format!(
90                    "Reorder index {old_idx} out of bounds (count={})",
91                    self.count
92                )));
93            }
94            // SAFETY: src is within the current allocation (old_idx < count, count <= capacity).
95            // dst is within the new allocation (new_idx < new_order.len() == count).
96            // Both buffers are distinct (non-overlapping) allocations with room for `dim` f32s.
97            // - Condition 1: old_idx < count ensures src offset is in bounds.
98            // - Condition 2: new_idx < count ensures dst offset is in bounds.
99            // SAFETY: Out-of-place copy for cache-locality reordering.
100            unsafe {
101                ptr::copy_nonoverlapping(
102                    self.data.as_ptr().add(old_idx * dim),
103                    dst.add(new_idx * dim),
104                    dim,
105                );
106            }
107        }
108        Ok(())
109    }
110
111    /// Computes dot product with another vector using SIMD.
112    #[inline]
113    #[must_use]
114    pub fn dot_product(&self, index: usize, query: &[f32]) -> Option<f32> {
115        let vector = self.get(index)?;
116        Some(crate::simd_native::dot_product_native(vector, query))
117    }
118
119    /// Prefetch distance for cache warming.
120    const PREFETCH_DISTANCE: usize = 4;
121
122    /// Computes batch dot products with a query vector.
123    ///
124    /// This is optimized for HNSW search with prefetching.
125    #[must_use]
126    pub fn batch_dot_products(&self, indices: &[usize], query: &[f32]) -> Vec<f32> {
127        let mut results = Vec::with_capacity(indices.len());
128
129        for (i, &idx) in indices.iter().enumerate() {
130            // Prefetch upcoming vectors
131            if i + Self::PREFETCH_DISTANCE < indices.len() {
132                self.prefetch(indices[i + Self::PREFETCH_DISTANCE]);
133            }
134
135            if let Some(score) = self.dot_product(idx, query) {
136                results.push(score);
137            }
138        }
139
140        results
141    }
142}
143
144impl Drop for ContiguousVectors {
145    fn drop(&mut self) {
146        // EPIC-032/US-002: No null check needed - NonNull guarantees non-null
147        // Layout was valid at construction; it must still be valid at drop.
148        let Ok(layout) = Self::layout(self.dimension, self.capacity) else {
149            // Layout was valid at construction; this branch is unreachable
150            // unless memory corruption occurred. Leak memory rather than abort.
151            tracing::error!(
152                "ContiguousVectors::drop: layout computation failed \
153                 (dim={}, cap={}), leaking memory",
154                self.dimension,
155                self.capacity,
156            );
157            return;
158        };
159        // SAFETY: data was allocated with this layout, is non-null (NonNull invariant)
160        // - Condition 1: Layout matches original allocation parameters.
161        // - Condition 2: Pointer is non-null per NonNull invariant.
162        // SAFETY: Release allocated memory when ContiguousVectors is dropped.
163        unsafe {
164            dealloc(self.data.as_ptr().cast::<u8>(), layout);
165        }
166    }
167}
168
169// =============================================================================
170// Batch Distance Computation (free functions)
171// =============================================================================
172
173/// Computes multiple dot products in a single pass (cache-optimized).
174///
175/// F-17: Delegates to `batch_dot_product_native` which includes `x86_64`
176/// prefetch hints for upcoming candidate vectors.
177#[must_use]
178pub fn batch_dot_products_simd(vectors: &[&[f32]], query: &[f32]) -> Vec<f32> {
179    crate::simd_native::batch_dot_product_native(vectors, query)
180}
181
182// =============================================================================
183// SIMD Padding Utility
184// =============================================================================
185
186/// AVX2 register width for `f32` lanes: 256 bits / 32 bits = 8 lanes.
187const SIMD_WIDTH: usize = 8;
188
189/// Pads a vector to the next multiple of 8 (AVX2 register width for `f32`).
190///
191/// Appending zeros does not affect distance computations (cosine, euclidean, dot)
192/// when the query and stored vectors share the same padded length.
193///
194/// Returns an empty `Vec` when the input is empty (0 is already a multiple of 8).
195///
196/// # Examples
197///
198/// ```
199/// use velesdb_core::contiguous_ops::pad_to_simd_width;
200///
201/// let v = vec![1.0_f32, 2.0, 3.0];
202/// let padded = pad_to_simd_width(&v);
203/// assert_eq!(padded.len(), 8);
204/// assert_eq!(&padded[..3], &[1.0, 2.0, 3.0]);
205/// ```
206#[must_use]
207pub fn pad_to_simd_width(vector: &[f32]) -> Vec<f32> {
208    let len = vector.len();
209    if len == 0 {
210        return Vec::new();
211    }
212    let padded_len = len.div_ceil(SIMD_WIDTH) * SIMD_WIDTH;
213    let mut padded = vec![0.0_f32; padded_len];
214    padded[..len].copy_from_slice(vector);
215    padded
216}
217
218/// Computes multiple cosine similarities in a single pass with prefetch.
219#[must_use]
220pub fn batch_cosine_similarities(vectors: &[&[f32]], query: &[f32]) -> Vec<f32> {
221    let prefetch_distance = crate::simd_native::calculate_prefetch_distance(query.len());
222    let mut results = Vec::with_capacity(vectors.len());
223
224    for (i, v) in vectors.iter().enumerate() {
225        if i + prefetch_distance < vectors.len() {
226            crate::simd_native::prefetch_vector(vectors[i + prefetch_distance]);
227        }
228        results.push(crate::simd_native::cosine_similarity_native(v, query));
229    }
230
231    results
232}