Skip to main content

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