Skip to main content

hermes_simd_core/vec/
mod.rs

1//! Custom aligned vector allocation for zero-copy aligned SIMD memory access.
2
3extern crate alloc;
4
5use crate::align::Alignment;
6use crate::numa::NumaAllocator;
7use crate::view::SimdView;
8use core::alloc::Layout;
9use core::marker::PhantomData;
10use core::ops::{Deref, DerefMut};
11
12#[cfg(not(feature = "mnemosyne-memory"))]
13use alloc::alloc::{alloc, dealloc};
14
15/// Zero-copy serialization support for aligned vectors using `rkyv`.
16pub mod rkyv;
17#[cfg(test)]
18mod tests;
19
20/// A heap-allocated vector with statically guaranteed memory alignment layout.
21///
22/// Underpinned by custom memory allocation using `core::alloc::Layout` to ensure that
23/// loading elements into SIMD registers does not trigger alignment faults.
24pub struct AlignedVec<T, Align: Alignment> {
25    ptr: *mut T,
26    len: usize,
27    cap: usize,
28    node: Option<u32>,
29    alloc_align: u32,
30    _marker: PhantomData<(T, Align)>,
31}
32
33unsafe impl<T: Send, Align: Alignment> Send for AlignedVec<T, Align> {}
34unsafe impl<T: Sync, Align: Alignment> Sync for AlignedVec<T, Align> {}
35
36impl<T, Align> AlignedVec<T, Align>
37where
38    Align: Alignment,
39{
40    #[inline(always)]
41    fn layout_for_capacity(capacity: usize, align: usize) -> Layout {
42        let size = capacity
43            .checked_mul(core::mem::size_of::<T>())
44            .expect("Capacity overflow");
45        Layout::from_size_align(size, align)
46            .expect("align is power-of-2, size validated by checked_mul")
47    }
48
49    /// Create a new empty `AlignedVec` with no allocation.
50    #[inline]
51    pub fn new() -> Self {
52        Self {
53            ptr: core::ptr::NonNull::dangling().as_ptr(),
54            len: 0,
55            cap: 0,
56            node: None,
57            alloc_align: if Align::IS_ALIGNED {
58                Align::ALIGN_BYTES as u32
59            } else {
60                core::mem::align_of::<T>() as u32
61            },
62            _marker: PhantomData,
63        }
64    }
65
66    /// Create a new `AlignedVec` with space allocated for `capacity` elements
67    /// satisfying the alignment boundary constraints.
68    pub fn with_capacity(capacity: usize) -> Self {
69        let default_align = if Align::IS_ALIGNED {
70            Align::ALIGN_BYTES as u32
71        } else {
72            core::mem::align_of::<T>() as u32
73        };
74        if core::mem::size_of::<T>() == 0 {
75            return Self {
76                ptr: core::ptr::NonNull::dangling().as_ptr(),
77                len: 0,
78                cap: usize::MAX,
79                node: None,
80                alloc_align: default_align,
81                _marker: PhantomData,
82            };
83        }
84        if capacity == 0 {
85            return Self::new();
86        }
87
88        let align = if Align::IS_ALIGNED {
89            Align::ALIGN_BYTES
90        } else {
91            core::mem::align_of::<T>()
92        };
93        let layout = Self::layout_for_capacity(capacity, align);
94
95        #[cfg(feature = "mnemosyne-memory")]
96        let ptr =
97            unsafe { core::alloc::GlobalAlloc::alloc(&mnemosyne::Mnemosyne, layout) as *mut T };
98        #[cfg(not(feature = "mnemosyne-memory"))]
99        let ptr = unsafe { alloc(layout) as *mut T };
100
101        if ptr.is_null() {
102            alloc::alloc::handle_alloc_error(layout);
103        }
104
105        Self {
106            ptr,
107            len: 0,
108            cap: capacity,
109            node: None,
110            alloc_align: align as u32,
111            _marker: PhantomData,
112        }
113    }
114
115    /// Create a new `AlignedVec` with space allocated for `capacity` elements
116    /// on the specified NUMA node.
117    pub fn with_capacity_numa(capacity: usize, node: u32) -> Self {
118        let default_align = if Align::IS_ALIGNED {
119            Align::ALIGN_BYTES as u32
120        } else {
121            core::mem::align_of::<T>() as u32
122        };
123        if core::mem::size_of::<T>() == 0 {
124            return Self {
125                ptr: core::ptr::NonNull::dangling().as_ptr(),
126                len: 0,
127                cap: usize::MAX,
128                node: Some(node),
129                alloc_align: default_align,
130                _marker: PhantomData,
131            };
132        }
133        if capacity == 0 {
134            return Self {
135                ptr: core::ptr::NonNull::dangling().as_ptr(),
136                len: 0,
137                cap: 0,
138                node: Some(node),
139                alloc_align: default_align,
140                _marker: PhantomData,
141            };
142        }
143
144        let align = if Align::IS_ALIGNED {
145            Align::ALIGN_BYTES
146        } else {
147            core::mem::align_of::<T>()
148        };
149        let layout = Self::layout_for_capacity(capacity, align);
150
151        let allocator = crate::numa::MnemosyneNumaAllocator;
152        let ptr = unsafe { allocator.alloc_on_node(layout, node) as *mut T };
153        if ptr.is_null() {
154            alloc::alloc::handle_alloc_error(layout);
155        }
156
157        Self {
158            ptr,
159            len: 0,
160            cap: capacity,
161            node: Some(node),
162            alloc_align: align as u32,
163            _marker: PhantomData,
164        }
165    }
166
167    /// Appends an element to the back of the vector.
168    pub fn push(&mut self, value: T) {
169        if core::mem::size_of::<T>() == 0 {
170            // No allocation or writes needed for zero-sized types.
171            // Bypasses drop of `value` when returning.
172            core::mem::forget(value);
173            self.len = self.len.checked_add(1).expect("Length overflow");
174            self.cap = usize::MAX;
175            return;
176        }
177        if self.len == self.cap {
178            self.grow();
179        }
180        unsafe {
181            core::ptr::write(self.ptr.add(self.len), value);
182            self.len += 1;
183        }
184    }
185
186    /// Reserve capacity for at least `additional` more elements beyond `len` in
187    /// a single reallocation; a no-op when capacity already suffices.
188    ///
189    /// Growth is geometric — the new capacity is at least double the old — so a
190    /// sequence of `reserve`/`push` calls keeps amortized O(1) append while a
191    /// one-shot `reserve(n)` before a bulk append performs exactly one
192    /// allocation instead of the `⌈log₂ n⌉` reallocations a push loop incurs.
193    ///
194    /// # Panics
195    /// If `len + additional` overflows `usize`, or on allocator failure.
196    pub fn reserve(&mut self, additional: usize) {
197        if core::mem::size_of::<T>() == 0 {
198            self.cap = usize::MAX;
199            return;
200        }
201        let needed = self.len.checked_add(additional).expect("Capacity overflow");
202        if needed <= self.cap {
203            return;
204        }
205        // Grow to at least `needed`, but never less than doubling, so a bulk
206        // reserve honors the exact target while incremental reserves preserve
207        // the geometric-growth amortization.
208        let new_cap = needed.max(self.cap.saturating_mul(2)).max(4);
209        self.grow_to(new_cap);
210    }
211
212    /// Append every element of `src` in a single reserve + `copy_nonoverlapping`.
213    ///
214    /// For `T: Copy` this is the bulk counterpart of [`push`](Self::push): one
215    /// allocation sized to fit (via [`reserve`](Self::reserve)) then one
216    /// contiguous memcpy, versus a per-element push loop's repeated bounds check
217    /// and geometric reallocations.
218    pub fn extend_from_slice(&mut self, src: &[T])
219    where
220        T: Copy,
221    {
222        self.reserve(src.len());
223        if src.is_empty() {
224            return;
225        }
226        // SAFETY: `reserve(src.len())` guaranteed `cap ≥ len + src.len()`, so the
227        // destination `[len, len + src.len())` is within the allocation and
228        // disjoint from `src` (distinct allocations). `T: Copy` ⇒ no drop/overlap
229        // hazard. For a ZST the copy is a no-op on the dangling-but-aligned ptr.
230        unsafe {
231            core::ptr::copy_nonoverlapping(src.as_ptr(), self.ptr.add(self.len), src.len());
232            self.len += src.len();
233        }
234    }
235
236    /// Returns the number of elements in the vector.
237    #[inline(always)]
238    pub fn len(&self) -> usize {
239        self.len
240    }
241
242    /// Returns true if the vector contains no elements.
243    #[inline(always)]
244    pub fn is_empty(&self) -> bool {
245        self.len == 0
246    }
247
248    /// Returns the capacity of the vector.
249    #[inline(always)]
250    pub fn capacity(&self) -> usize {
251        self.cap
252    }
253
254    /// Returns a raw pointer to the vector's buffer.
255    #[inline(always)]
256    pub fn as_ptr(&self) -> *const T {
257        self.ptr
258    }
259
260    /// Returns a raw mutable pointer to the vector's buffer.
261    #[inline(always)]
262    pub fn as_mut_ptr(&mut self) -> *mut T {
263        self.ptr
264    }
265
266    /// Returns the reserved-but-uninitialized tail `[len, capacity)` as a slice
267    /// of `MaybeUninit<T>`.
268    ///
269    /// A routine that fills this slice and then advances the length with
270    /// [`set_len`](Self::set_len) initializes each element exactly once with no
271    /// intervening zero-fill — the memory-efficient alternative to reserving,
272    /// zeroing, and overwriting. Immediately after `with_capacity(n)` the length
273    /// is zero, so this covers the whole `n`-element allocation.
274    #[inline(always)]
275    pub fn spare_capacity_mut(&mut self) -> &mut [core::mem::MaybeUninit<T>] {
276        // SAFETY: `ptr` is valid for `cap` elements and `len <= cap`, so the
277        // `[len, cap)` region lies within the allocation. `MaybeUninit<T>` has
278        // the same layout as `T` and imposes no initialization invariant, so a
279        // mutable slice of it over reserved capacity is sound even though those
280        // elements are not yet initialized.
281        unsafe {
282            core::slice::from_raw_parts_mut(
283                self.ptr.add(self.len) as *mut core::mem::MaybeUninit<T>,
284                self.cap - self.len,
285            )
286        }
287    }
288
289    /// Forcefully sets the length of the vector without initializing elements.
290    ///
291    /// # Safety
292    ///
293    /// The elements up to `new_len` must be initialized.
294    #[inline(always)]
295    pub unsafe fn set_len(&mut self, new_len: usize) {
296        debug_assert!(new_len <= self.cap);
297        self.len = new_len;
298    }
299
300    /// Accesses the elements as an immutable slice.
301    #[inline(always)]
302    pub fn as_slice(&self) -> &[T] {
303        if self.len == 0 {
304            &[]
305        } else {
306            unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
307        }
308    }
309
310    /// Accesses the elements as a mutable slice.
311    #[inline(always)]
312    pub fn as_mut_slice(&mut self) -> &mut [T] {
313        if self.len == 0 {
314            &mut []
315        } else {
316            unsafe { core::slice::from_raw_parts_mut(self.ptr, self.len) }
317        }
318    }
319
320    /// Copy `src` into a new `AlignedVec` in a single allocation.
321    ///
322    /// # Performance
323    ///
324    /// Exactly one call to the allocator (`with_capacity(src.len())`) followed by
325    /// one `copy_nonoverlapping` of `src.len()` elements. Zero intermediate allocations.
326    /// The returned vec is fully owned and has `len == cap == src.len()`.
327    #[inline]
328    pub fn from_slice(src: &[T]) -> Self
329    where
330        T: Copy,
331    {
332        let n = src.len();
333        if n == 0 {
334            return Self::new();
335        }
336        if core::mem::size_of::<T>() == 0 {
337            let mut v = Self::new();
338            v.len = n;
339            v.cap = usize::MAX;
340            return v;
341        }
342        let mut v = Self::with_capacity(n);
343        unsafe {
344            core::ptr::copy_nonoverlapping(src.as_ptr(), v.ptr, n);
345            v.len = n;
346        }
347        v
348    }
349
350    /// Clone elements from `src` into a new `AlignedVec` in a single allocation.
351    ///
352    /// # Performance
353    ///
354    /// Exactly one call to the allocator (`with_capacity(src.len())`) followed by
355    /// cloning elements into place sequentially.
356    #[inline]
357    pub fn from_slice_clone(src: &[T]) -> Self
358    where
359        T: Clone,
360    {
361        let n = src.len();
362        if n == 0 {
363            return Self::new();
364        }
365        if core::mem::size_of::<T>() == 0 {
366            let mut v = Self::new();
367            v.len = n;
368            v.cap = usize::MAX;
369            return v;
370        }
371        let mut v = Self::with_capacity(n);
372        for i in 0..n {
373            unsafe {
374                core::ptr::write(v.ptr.add(i), src[i].clone());
375                v.len = i + 1;
376            }
377        }
378        v
379    }
380
381    /// Obtains a compile-time safe immutable `SimdView` over the vector's buffer.
382    #[inline(always)]
383    pub fn view<'a, Arch>(
384        &'a self,
385    ) -> SimdView<'a, T, Arch, Align, crate::execution::Unmasked, &'a [T]>
386    where
387        Arch: crate::arch::SimdArch,
388    {
389        SimdView::new(self.as_slice())
390            .expect("AlignedVec guarantees aligned buffer of sufficient length")
391    }
392
393    /// Obtains a compile-time safe mutable `SimdView` over the vector's buffer.
394    #[inline(always)]
395    pub fn view_mut<'a, Arch>(
396        &'a mut self,
397    ) -> SimdView<'a, T, Arch, Align, crate::execution::Unmasked, &'a mut [T]>
398    where
399        Arch: crate::arch::SimdArch,
400    {
401        SimdView::new_mut(self.as_mut_slice())
402            .expect("AlignedVec guarantees aligned buffer of sufficient length")
403    }
404
405    /// Converts this `AlignedVec` to another alignment layout type-safely, without checking
406    /// if the pointer satisfies the new alignment's constraints.
407    ///
408    /// # Safety
409    ///
410    /// The caller must guarantee that the underlying memory address satisfies the alignment
411    /// boundary constraints of `NewAlign`.
412    #[inline(always)]
413    pub unsafe fn into_alignment_unchecked<NewAlign: Alignment>(self) -> AlignedVec<T, NewAlign> {
414        let md = core::mem::ManuallyDrop::new(self);
415        AlignedVec {
416            ptr: md.ptr,
417            len: md.len,
418            cap: md.cap,
419            node: md.node,
420            alloc_align: md.alloc_align,
421            _marker: PhantomData,
422        }
423    }
424
425    /// Converts this `AlignedVec` to an unaligned layout, stripping the alignment guarantee zero-cost.
426    #[inline(always)]
427    pub fn into_unaligned(self) -> AlignedVec<T, crate::align::Unaligned> {
428        unsafe { self.into_alignment_unchecked() }
429    }
430
431    /// Attempts to cast this `AlignedVec` to a stricter alignment constraint.
432    /// Returns `Some` if the pointer satisfies the alignment requirement of `NewAlign`, otherwise `None`.
433    #[inline]
434    pub fn try_into_alignment<NewAlign: Alignment>(self) -> Option<AlignedVec<T, NewAlign>> {
435        if NewAlign::IS_ALIGNED {
436            let addr = self.as_ptr() as usize;
437            if addr % NewAlign::ALIGN_BYTES == 0 {
438                unsafe { Some(self.into_alignment_unchecked()) }
439            } else {
440                None
441            }
442        } else {
443            unsafe { Some(self.into_alignment_unchecked()) }
444        }
445    }
446
447    fn layout_for(&self, capacity: usize) -> Layout {
448        Self::layout_for_capacity(capacity, self.alloc_align as usize)
449    }
450
451    fn grow(&mut self) {
452        if core::mem::size_of::<T>() == 0 {
453            self.cap = usize::MAX;
454            return;
455        }
456
457        let new_cap = if self.cap == 0 {
458            4
459        } else {
460            self.cap.checked_mul(2).expect("Capacity overflow")
461        };
462        self.grow_to(new_cap);
463    }
464
465    /// Reallocate the backing storage to exactly `new_cap` elements in a single
466    /// allocator call, preserving the existing `len` initialized elements.
467    ///
468    /// SSOT for capacity growth: [`grow`] (geometric doubling for `push`) and
469    /// [`reserve`](Self::reserve) (grow to an explicit target) both delegate here
470    /// so the NUMA / mnemosyne / global-allocator branch logic lives once.
471    ///
472    /// # Panics
473    /// On allocator failure (`handle_alloc_error`) or capacity-layout overflow.
474    /// Caller guarantees `T` is not a ZST and `new_cap > self.cap`.
475    fn grow_to(&mut self, new_cap: usize) {
476        let new_layout = self.layout_for(new_cap);
477
478        let old_ptr = self.ptr;
479        let new_ptr = if self.cap == 0 {
480            if let Some(node) = self.node {
481                let allocator = crate::numa::MnemosyneNumaAllocator;
482                unsafe { allocator.alloc_on_node(new_layout, node) as *mut T }
483            } else {
484                #[cfg(feature = "mnemosyne-memory")]
485                unsafe {
486                    core::alloc::GlobalAlloc::alloc(&mnemosyne::Mnemosyne, new_layout) as *mut T
487                }
488                #[cfg(not(feature = "mnemosyne-memory"))]
489                unsafe {
490                    alloc(new_layout) as *mut T
491                }
492            }
493        } else {
494            let old_layout = self.layout_for(self.cap);
495            unsafe {
496                if let Some(node) = self.node {
497                    let allocator = crate::numa::MnemosyneNumaAllocator;
498                    allocator.realloc_on_node(self.ptr as *mut u8, old_layout, new_layout, node)
499                        as *mut T
500                } else {
501                    #[cfg(feature = "mnemosyne-memory")]
502                    let ptr = core::alloc::GlobalAlloc::realloc(
503                        &mnemosyne::Mnemosyne,
504                        self.ptr as *mut u8,
505                        old_layout,
506                        new_layout.size(),
507                    ) as *mut T;
508                    #[cfg(not(feature = "mnemosyne-memory"))]
509                    let ptr =
510                        alloc::alloc::realloc(self.ptr as *mut u8, old_layout, new_layout.size())
511                            as *mut T;
512                    ptr
513                }
514            }
515        };
516
517        if new_ptr.is_null() {
518            alloc::alloc::handle_alloc_error(new_layout);
519        }
520
521        if self.node.is_none() && self.cap > 0 && new_ptr != old_ptr {
522            crate::numa::locality::bump_alloc_generation();
523        }
524
525        self.ptr = new_ptr;
526        self.cap = new_cap;
527    }
528}
529
530impl<T, Align: Alignment> Deref for AlignedVec<T, Align> {
531    type Target = [T];
532
533    #[inline(always)]
534    fn deref(&self) -> &Self::Target {
535        self.as_slice()
536    }
537}
538
539impl<T, Align: Alignment> DerefMut for AlignedVec<T, Align> {
540    #[inline(always)]
541    fn deref_mut(&mut self) -> &mut Self::Target {
542        self.as_mut_slice()
543    }
544}
545
546struct DeallocGuard<T, Align: Alignment> {
547    ptr: *mut T,
548    cap: usize,
549    node: Option<u32>,
550    alloc_align: u32,
551    _marker: PhantomData<(T, Align)>,
552}
553
554impl<T, Align: Alignment> Drop for DeallocGuard<T, Align> {
555    fn drop(&mut self) {
556        if !self.ptr.is_null() && self.cap > 0 {
557            crate::numa::locality::bump_alloc_generation();
558            unsafe {
559                let layout = AlignedVec::<T, Align>::layout_for_capacity(
560                    self.cap,
561                    self.alloc_align as usize,
562                );
563                if let Some(node) = self.node {
564                    let allocator = crate::numa::MnemosyneNumaAllocator;
565                    allocator.dealloc_on_node(self.ptr as *mut u8, layout, node);
566                } else {
567                    #[cfg(feature = "mnemosyne-memory")]
568                    core::alloc::GlobalAlloc::dealloc(
569                        &mnemosyne::Mnemosyne,
570                        self.ptr as *mut u8,
571                        layout,
572                    );
573                    #[cfg(not(feature = "mnemosyne-memory"))]
574                    dealloc(self.ptr as *mut u8, layout);
575                }
576            }
577        }
578    }
579}
580
581impl<T, Align: Alignment> Drop for AlignedVec<T, Align> {
582    fn drop(&mut self) {
583        if core::mem::size_of::<T>() == 0 {
584            if self.len > 0 {
585                unsafe {
586                    core::ptr::drop_in_place(core::ptr::slice_from_raw_parts_mut(
587                        self.ptr, self.len,
588                    ));
589                }
590            }
591            return;
592        }
593        if !self.ptr.is_null() && self.cap > 0 {
594            let ptr = self.ptr;
595            let cap = self.cap;
596            let len = self.len;
597            let alloc_align = self.alloc_align;
598
599            self.ptr = core::ptr::null_mut();
600            self.cap = 0;
601            self.len = 0;
602
603            let _guard: DeallocGuard<T, Align> = DeallocGuard {
604                ptr,
605                cap,
606                node: self.node,
607                alloc_align,
608                _marker: PhantomData,
609            };
610            unsafe {
611                core::ptr::drop_in_place(core::ptr::slice_from_raw_parts_mut(ptr, len));
612            }
613        }
614    }
615}
616
617impl<T: Clone, Align: Alignment> Clone for AlignedVec<T, Align> {
618    fn clone(&self) -> Self {
619        if core::mem::size_of::<T>() == 0 {
620            let mut new_vec = Self {
621                ptr: core::ptr::NonNull::dangling().as_ptr(),
622                len: 0,
623                cap: usize::MAX,
624                node: self.node,
625                alloc_align: self.alloc_align,
626                _marker: PhantomData,
627            };
628            for val in self.as_slice() {
629                new_vec.push(val.clone());
630            }
631            return new_vec;
632        }
633        let mut new_vec = if let Some(node) = self.node {
634            Self::with_capacity_numa(self.len, node)
635        } else {
636            Self::with_capacity(self.len)
637        };
638        for i in 0..self.len {
639            unsafe {
640                let val = (*self.ptr.add(i)).clone();
641                core::ptr::write(new_vec.ptr.add(i), val);
642                new_vec.len = i + 1;
643            }
644        }
645        new_vec
646    }
647}
648
649impl<T, Align: Alignment> Default for AlignedVec<T, Align> {
650    #[inline]
651    fn default() -> Self {
652        Self::new()
653    }
654}
655
656impl<T: core::fmt::Debug, Align: Alignment> core::fmt::Debug for AlignedVec<T, Align> {
657    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
658        core::fmt::Debug::fmt(self.as_slice(), f)
659    }
660}
661
662impl<T: PartialEq, Align1: Alignment, Align2: Alignment> PartialEq<AlignedVec<T, Align2>>
663    for AlignedVec<T, Align1>
664{
665    #[inline]
666    fn eq(&self, other: &AlignedVec<T, Align2>) -> bool {
667        self.as_slice() == other.as_slice()
668    }
669}
670
671impl<T: Eq, Align: Alignment> Eq for AlignedVec<T, Align> {}
672
673impl<T: PartialEq, Align: Alignment> PartialEq<[T]> for AlignedVec<T, Align> {
674    #[inline]
675    fn eq(&self, other: &[T]) -> bool {
676        self.as_slice() == other
677    }
678}
679
680impl<T: PartialEq, Align: Alignment> PartialEq<AlignedVec<T, Align>> for [T] {
681    #[inline]
682    fn eq(&self, other: &AlignedVec<T, Align>) -> bool {
683        self == other.as_slice()
684    }
685}