Skip to main content

hermes_simd_core/cow/
extensions.rs

1//! Unary map, in-place scale, splat-fill, argmin/argmax, gather, and prefix-scan
2//! extensions for `SimdCow`.
3//!
4//! # Safety
5//!
6//! Two obligations recur here. Kernel calls are `#[target_feature]`-gated, and
7//! that precondition holds by construction: a `SimdCow` exists only for an
8//! architecture the host can execute, since its borrowed form comes from
9//! [`SimdView::new`](crate::view::SimdView::new) and its owned constructors
10//! assert the same condition. The second is local — these routines build their
11//! output buffer with `with_capacity` and write it through a raw pointer,
12//! raising the length only once every element is initialized. That avoids both
13//! a zero-fill of a buffer about to be overwritten and any `&mut [T]` spanning
14//! uninitialized elements, so each such site carries a `SAFETY` comment showing
15//! the write coverage. `gather` and `prefix_scan` reserve capacity and fill it
16//! through the view's `*_into_uninit` methods over
17//! [`AlignedVec::spare_capacity_mut`](crate::vec::AlignedVec::spare_capacity_mut),
18//! then raise the length once those report success, so those paths never zero
19//! the buffer either.
20
21use super::types::SimdCow;
22use crate::align::Alignment;
23use crate::arch::SimdArch;
24use crate::kernel::SimdKernel;
25use crate::scalar::Scalar;
26use crate::vec::AlignedVec;
27use crate::view::SimdError;
28
29impl<'a, T: 'a, Arch, Align> SimdCow<'a, T, Arch, Align>
30where
31    T: Scalar,
32    Arch: SimdArch + SimdKernel<T>,
33    Align: Alignment,
34{
35    /// Apply a `UnaryOp<T>` to every element, returning a fully-owned
36    /// `SimdCow<'static, T, Arch, Align>` backed by a single `AlignedVec` allocation.
37    ///
38    /// Zero intermediate copies: one allocation, one vectorized pass.
39    #[inline]
40    pub fn map_unary<Op: crate::ops::UnaryOp<T>>(
41        &self,
42        op: Op,
43    ) -> SimdCow<'static, T, Arch, Align> {
44        // Same operation as `map_cow`, which owns the single implementation:
45        // it writes the output buffer through a raw pointer and raises the
46        // length only once every element is initialized.
47        self.map_cow(op)
48    }
49
50    /// Apply a `UnaryOp<T>` in-place: `self[i] = op(self[i])`.
51    ///
52    /// Promotes `self` to owned if currently borrowed (one allocation).
53    /// Subsequent calls on the same already-owned `SimdCow` are allocation-free.
54    #[inline]
55    pub fn map_unary_in_place<Op: crate::ops::UnaryOp<T>>(&mut self, op: Op) {
56        self.view_mut().map_unary_in_place(op);
57    }
58
59    /// Multiply every element by `scalar` in-place: `self[i] *= scalar`.
60    ///
61    /// Uses `Arch::splat(scalar)` + `Arch::mul` to broadcast-multiply without
62    /// a second `SimdCow`. Promotes to owned if currently borrowed (one allocation).
63    #[inline]
64    pub fn scale_in_place(&mut self, scalar: T) {
65        let len = self.len();
66        if len == 0 {
67            return;
68        }
69        let lane_count = Arch::LANE_COUNT;
70        let simd_len = (len / lane_count) * lane_count;
71
72        let vec = self.to_mut();
73        let ptr = vec.as_mut_ptr();
74
75        unsafe {
76            let vsplat = Arch::splat(scalar);
77
78            let load = |p: *const T| -> Arch::Vector {
79                if crate::align::is_aligned_for_arch::<Arch, Align>() {
80                    Arch::load_aligned(p)
81                } else {
82                    Arch::load_unaligned(p)
83                }
84            };
85            let store = |p: *mut T, v: Arch::Vector| {
86                if crate::align::is_aligned_for_arch::<Arch, Align>() {
87                    Arch::store_aligned(p, v);
88                } else {
89                    Arch::store_unaligned(p, v);
90                }
91            };
92
93            let mut i = 0usize;
94            while i < simd_len {
95                let p = ptr.add(i);
96                let v = load(p);
97                store(p, Arch::mul(v, vsplat));
98                i += lane_count;
99            }
100        }
101
102        // Scalar tail
103        let slice = vec.as_mut_slice();
104        for i in simd_len..len {
105            slice[i] = slice[i] * scalar;
106        }
107    }
108
109    /// Return an owned `SimdCow` with every element multiplied by `scalar`.
110    ///
111    /// One allocation. Delegates to the fused [`SimdCow::mul_scalar_cow`]
112    /// broadcast kernel (single read+write pass); the previous copy-then-
113    /// `scale_in_place` body cost a second full read+write pass over the
114    /// buffer for a bitwise-identical result.
115    #[inline]
116    pub fn scale(&self, scalar: T) -> SimdCow<'static, T, Arch, Align> {
117        self.mul_scalar_cow(scalar)
118    }
119
120    /// Construct an owned `SimdCow` of length `len` with every element set to `value`.
121    ///
122    /// Uses `Arch::splat` + `Arch::store_unaligned` for the SIMD prefix;
123    /// scalar assignment for the tail. One allocation.
124    #[inline]
125    pub fn splat_fill(value: T, len: usize) -> SimdCow<'static, T, Arch, Align> {
126        let mut out: AlignedVec<T, Align> = AlignedVec::with_capacity(len);
127        let lane_count = Arch::LANE_COUNT;
128        let simd_len = (len / lane_count) * lane_count;
129        let ptr = out.as_mut_ptr();
130
131        // SAFETY: `with_capacity(len)` reserved `len` elements, so every store
132        // below `len` stays inside the allocation. The vector's length is
133        // raised only after the vector and scalar loops have together written
134        // all `len` elements, so no reference spans uninitialized memory.
135        unsafe {
136            let vsplat = Arch::splat(value);
137            let mut i = 0usize;
138            while i < simd_len {
139                if crate::align::is_aligned_for_arch::<Arch, Align>() {
140                    Arch::store_aligned(ptr.add(i), vsplat);
141                } else {
142                    Arch::store_unaligned(ptr.add(i), vsplat);
143                }
144                i += lane_count;
145            }
146            for i in simd_len..len {
147                core::ptr::write(ptr.add(i), value);
148            }
149            out.set_len(len);
150        }
151
152        SimdCow::Owned(out)
153    }
154
155    /// Construct an owned `SimdCow` of length `len` filled with `T::ZERO`.
156    #[inline]
157    pub fn zeros(len: usize) -> SimdCow<'static, T, Arch, Align> {
158        Self::splat_fill(T::ZERO, len)
159    }
160
161    /// Construct an owned `SimdCow` of length `len` filled with `T::ONE`.
162    #[inline]
163    pub fn ones(len: usize) -> SimdCow<'static, T, Arch, Align> {
164        Self::splat_fill(T::ONE, len)
165    }
166
167    /// Returns the first minimum, or `None` for empty or NaN-containing data.
168    #[inline]
169    pub fn argmin(&self) -> Option<(usize, T)>
170    where
171        T: crate::scalar::NumericElement,
172    {
173        self.view().argmin()
174    }
175
176    /// Returns the first maximum, or `None` for empty or NaN-containing data.
177    #[inline]
178    pub fn argmax(&self) -> Option<(usize, T)>
179    where
180        T: crate::scalar::NumericElement,
181    {
182        self.view().argmax()
183    }
184
185    /// Indirectly load (gather) elements from this view using indices, returning a new owned `SimdCow`.
186    ///
187    /// # Errors
188    /// Returns `SimdError::IndexOutOfBounds` if any index in `indices` is out of bounds.
189    #[inline]
190    pub fn gather(&self, indices: &[i32]) -> Result<SimdCow<'static, T, Arch, Align>, SimdError> {
191        let len = indices.len();
192        let mut out = AlignedVec::with_capacity(len);
193        // Gather fills the reserved capacity directly and reports how many
194        // elements it wrote; nothing is written on the error path, so `out`
195        // stays length-zero and drops no uninitialized element.
196        self.view()
197            .gather_into_uninit(indices, out.spare_capacity_mut())?;
198        // SAFETY: `gather_into_uninit` returned `Ok`, so it initialized exactly
199        // `len` elements of the reserved capacity.
200        unsafe { out.set_len(len) };
201        Ok(SimdCow::Owned(out))
202    }
203
204    /// Perform a prefix scan (inclusive or exclusive) of the view using the specified operation,
205    /// returning a new owned `SimdCow`.
206    #[inline]
207    pub fn prefix_scan<Op, SMode>(
208        &self,
209        op: Op,
210        mode: SMode,
211    ) -> Result<SimdCow<'static, T, Arch, Align>, SimdError>
212    where
213        Op: crate::ops::ScanOp<T>,
214        SMode: crate::ops::ScanMode,
215    {
216        let len = self.len();
217        let mut out = AlignedVec::with_capacity(len);
218        // Scan fills the reserved capacity directly; the only error is an
219        // insufficient-length one it checks before writing, so on error `out`
220        // stays length-zero and drops no uninitialized element.
221        self.view()
222            .prefix_scan_into_uninit(out.spare_capacity_mut(), op, mode)?;
223        // SAFETY: `prefix_scan_into_uninit` returned `Ok`, so it initialized
224        // exactly `len` elements of the reserved capacity.
225        unsafe { out.set_len(len) };
226        Ok(SimdCow::Owned(out))
227    }
228
229    /// Perform an in-place prefix scan (inclusive or exclusive) of the view using the specified operation.
230    ///
231    /// Promotes `self` to owned if currently borrowed (one allocation).
232    /// Subsequent calls on the same already-owned `SimdCow` are allocation-free.
233    #[inline]
234    pub fn prefix_scan_in_place<Op, SMode>(&mut self, op: Op, mode: SMode) -> Result<(), SimdError>
235    where
236        Op: crate::ops::ScanOp<T>,
237        SMode: crate::ops::ScanMode,
238    {
239        // `view_mut` promotes borrowed → owned (one allocation if borrowed,
240        // free if owned). The scan itself is the single authoritative
241        // vectorized implementation on `SimdView`.
242        self.view_mut().prefix_scan_in_place(op, mode);
243        Ok(())
244    }
245}