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}