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}