Skip to main content

hermes_simd_core/iter/
zip.rs

1//! `ZipChunks` and `ZipChunksMut` — paired SIMD chunk iterators for two views.
2//!
3//! `ZipChunks` advances two immutable views in lockstep; `ZipChunksMut` pairs a
4//! mutable first operand with an immutable second operand for in-place transforms.
5//!
6//! # Safety
7//!
8//! Every kernel call below is `#[target_feature]`-gated and is therefore sound
9//! only on a host implementing `Arch`. That holds by construction rather than by
10//! inspection: [`SimdView::new`](crate::view::SimdView::new) returns `None` for
11//! an architecture the host cannot execute, and the sparse and copy-on-write
12//! constructors assert the same condition, so possessing one of these
13//! arch-parameterized values *is* the proof. Per-site `SAFETY` comments record
14//! only the obligations that go beyond it — pointer provenance, bounds, and
15//! alignment.
16
17use crate::align::Alignment;
18use crate::arch::SimdArch;
19use crate::execution::ExecutionMode;
20use crate::kernel::SimdKernel;
21use crate::scalar::Scalar;
22use crate::view::SimdView;
23
24// ---------------------------------------------------------------------------
25// ZipChunks — paired immutable/immutable
26// ---------------------------------------------------------------------------
27
28/// Iterator over non-overlapping paired `LANE_COUNT`-wide sub-views of two `SimdView`s.
29///
30/// Advances both views in lockstep, yielding pairs of chunks until the shorter
31/// view's SIMD prefix is exhausted.  Access the tails via [`ZipChunks::remainder`].
32///
33/// # Zero-Cost Guarantee
34/// Stores two base pointers, two positions, and one shared `simd_end` — 5 words
35/// on 64-bit targets. No heap allocation. `LANE_COUNT` is a compile-time constant so
36/// the advancement step optimizes identically to the single-view `SimdChunks` case.
37pub struct ZipChunks<'a, 'b, T, Arch: SimdArch, Align: Alignment, Mode: ExecutionMode> {
38    base_a: *const T,
39    base_b: *const T,
40    pos: usize,
41    simd_end: usize,
42    total_a: usize,
43    total_b: usize,
44    _marker: core::marker::PhantomData<(&'a T, &'b T, Arch, Align, Mode)>,
45}
46
47// SAFETY: ZipChunks borrows two `'a`/`'b` immutable slices; forwarding Send/Sync is sound.
48unsafe impl<'a, 'b, T: Send, Arch: SimdArch + SimdKernel<T>, Align: Alignment, Mode: ExecutionMode>
49    Send for ZipChunks<'a, 'b, T, Arch, Align, Mode>
50where
51    T: Scalar,
52{
53}
54unsafe impl<'a, 'b, T: Sync, Arch: SimdArch + SimdKernel<T>, Align: Alignment, Mode: ExecutionMode>
55    Sync for ZipChunks<'a, 'b, T, Arch, Align, Mode>
56where
57    T: Scalar,
58{
59}
60
61impl<'a, 'b, T: Scalar, Arch: SimdArch + SimdKernel<T>, Align: Alignment, Mode: ExecutionMode>
62    ZipChunks<'a, 'b, T, Arch, Align, Mode>
63{
64    /// Construct a `ZipChunks` from two slice pointers.
65    ///
66    /// # Safety
67    /// `base_a` must be valid for reads of `total_a` elements for `'a`.
68    /// `base_b` must be valid for reads of `total_b` elements for `'b`.
69    #[inline]
70    pub(crate) unsafe fn from_raw_parts(
71        base_a: *const T,
72        total_a: usize,
73        base_b: *const T,
74        total_b: usize,
75    ) -> Self {
76        let lane_count = Arch::LANE_COUNT;
77        let min_total = total_a.min(total_b);
78        let simd_end = (min_total / lane_count) * lane_count;
79        Self {
80            base_a,
81            base_b,
82            pos: 0,
83            simd_end,
84            total_a,
85            total_b,
86            _marker: core::marker::PhantomData,
87        }
88    }
89
90    /// Returns the scalar tails for both views.
91    #[inline(always)]
92    pub fn remainder(&self) -> (&'a [T], &'b [T]) {
93        // SAFETY: base + simd_end is within bounds by construction.
94        unsafe {
95            (
96                core::slice::from_raw_parts(
97                    self.base_a.add(self.simd_end),
98                    self.total_a - self.simd_end,
99                ),
100                core::slice::from_raw_parts(
101                    self.base_b.add(self.simd_end),
102                    self.total_b - self.simd_end,
103                ),
104            )
105        }
106    }
107
108    /// Returns the number of complete paired SIMD chunks remaining.
109    #[inline(always)]
110    pub fn chunks_remaining(&self) -> usize {
111        if self.simd_end > self.pos {
112            (self.simd_end - self.pos) / Arch::LANE_COUNT
113        } else {
114            0
115        }
116    }
117}
118
119impl<'a, 'b, T: Scalar, Arch: SimdArch + SimdKernel<T>, Align: Alignment, Mode: ExecutionMode>
120    Iterator for ZipChunks<'a, 'b, T, Arch, Align, Mode>
121{
122    type Item = (
123        SimdView<'a, T, Arch, Align, Mode, &'a [T]>,
124        SimdView<'b, T, Arch, Align, Mode, &'b [T]>,
125    );
126
127    #[inline(always)]
128    fn next(&mut self) -> Option<Self::Item> {
129        if self.pos >= self.simd_end {
130            return None;
131        }
132        // SAFETY: pos < simd_end <= min(total_a, total_b) <= total_a and total_b.
133        let (chunk_a, chunk_b) = unsafe {
134            (
135                core::slice::from_raw_parts(self.base_a.add(self.pos), Arch::LANE_COUNT),
136                core::slice::from_raw_parts(self.base_b.add(self.pos), Arch::LANE_COUNT),
137            )
138        };
139        self.pos += Arch::LANE_COUNT;
140        Some((
141            SimdView::new(chunk_a).expect("zip chunk_a alignment invariant violated"),
142            SimdView::new(chunk_b).expect("zip chunk_b alignment invariant violated"),
143        ))
144    }
145
146    #[inline(always)]
147    fn size_hint(&self) -> (usize, Option<usize>) {
148        let r = self.chunks_remaining();
149        (r, Some(r))
150    }
151}
152
153impl<'a, 'b, T: Scalar, Arch: SimdArch + SimdKernel<T>, Align: Alignment, Mode: ExecutionMode>
154    ExactSizeIterator for ZipChunks<'a, 'b, T, Arch, Align, Mode>
155{
156}
157
158impl<'a, 'b, T: Scalar, Arch: SimdArch + SimdKernel<T>, Align: Alignment, Mode: ExecutionMode>
159    core::iter::FusedIterator for ZipChunks<'a, 'b, T, Arch, Align, Mode>
160{
161}
162
163// ---------------------------------------------------------------------------
164// ZipChunksMut — paired mutable/immutable
165// ---------------------------------------------------------------------------
166
167/// Paired iterator over non-overlapping `LANE_COUNT`-wide sub-views where the first
168/// operand is **mutable** and the second is **immutable**.
169///
170/// Enables zero-copy 2-operand in-place transforms without unsafe pointer arithmetic
171/// at call sites. Canonical usage (SAXPY: `a[i] += s * b[i]`):
172///
173/// ```rust,ignore
174/// let mut chunks = view_a.zip_chunks_mut(&view_b);
175/// for (mut chunk_a, chunk_b) in &mut chunks {
176///     chunk_a.transform_in_place(&chunk_b, FmaAdd);
177/// }
178/// let (tail_a, tail_b) = chunks.into_remainder();
179/// ```
180///
181/// # Zero-Cost Guarantee
182///
183/// `ZipChunksMut` stores one `*mut T`, one `*const T`, and three `usize` — 5 words total.
184/// No heap allocation. `LANE_COUNT` is a compile-time constant.
185pub struct ZipChunksMut<'a, 'b, T: 'a + 'b, Arch: SimdArch, Align: Alignment, Mode: ExecutionMode> {
186    /// Base mutable pointer for the first (output) operand.
187    ptr_a: *mut T,
188    /// Base immutable pointer for the second (input) operand.
189    ptr_b: *const T,
190    /// Current element offset.
191    pos: usize,
192    /// Total elements in the shorter of the two slices.
193    total: usize,
194    /// `floor(total / LANE_COUNT) * LANE_COUNT` — the SIMD-processable prefix.
195    simd_end: usize,
196    _marker: core::marker::PhantomData<(&'a mut T, &'b T, Arch, Align, Mode)>,
197}
198
199// SAFETY: `ZipChunksMut` holds exclusive (`*mut T`) access to `'a` data and shared
200// (`*const T`) access to `'b` data. Forwarding Send/Sync is sound when `T: Send + Sync`.
201unsafe impl<'a, 'b, T, Arch, Align, Mode> Send for ZipChunksMut<'a, 'b, T, Arch, Align, Mode>
202where
203    T: Scalar + Send + Sync,
204    Arch: SimdArch + SimdKernel<T>,
205    Align: Alignment,
206    Mode: ExecutionMode,
207{
208}
209unsafe impl<'a, 'b, T, Arch, Align, Mode> Sync for ZipChunksMut<'a, 'b, T, Arch, Align, Mode>
210where
211    T: Scalar + Send + Sync,
212    Arch: SimdArch + SimdKernel<T>,
213    Align: Alignment,
214    Mode: ExecutionMode,
215{
216}
217
218impl<'a, 'b, T: 'a + 'b, Arch: SimdArch + SimdKernel<T>, Align: Alignment, Mode: ExecutionMode>
219    ZipChunksMut<'a, 'b, T, Arch, Align, Mode>
220where
221    T: Scalar,
222{
223    /// Create a new `ZipChunksMut` from raw pointer parts.
224    ///
225    /// # Safety
226    /// - `ptr_a` must be valid for **exclusive** reads and writes of `total_a` elements for `'a`.
227    /// - `ptr_b` must be valid for reads of `total_b` elements for `'b`.
228    /// - The two ranges must not overlap.
229    #[inline]
230    pub(crate) unsafe fn from_raw_parts(
231        ptr_a: *mut T,
232        total_a: usize,
233        ptr_b: *const T,
234        total_b: usize,
235    ) -> Self {
236        let lane_count = Arch::LANE_COUNT;
237        let total = total_a.min(total_b);
238        let simd_end = (total / lane_count) * lane_count;
239        Self {
240            ptr_a,
241            ptr_b,
242            pos: 0,
243            total,
244            simd_end,
245            _marker: core::marker::PhantomData,
246        }
247    }
248
249    /// Consume the iterator and return the scalar tail slices.
250    ///
251    /// Elements `[simd_end..total]` for each operand. Call this **after** the loop.
252    #[inline(always)]
253    pub fn into_remainder(self) -> (&'a mut [T], &'b [T]) {
254        let len = self.total - self.simd_end;
255        // SAFETY: ptr_a/ptr_b + simd_end is within bounds by construction; both
256        // lifetimes are preserved by the return type.
257        unsafe {
258            (
259                core::slice::from_raw_parts_mut(self.ptr_a.add(self.simd_end), len),
260                core::slice::from_raw_parts(self.ptr_b.add(self.simd_end), len),
261            )
262        }
263    }
264
265    /// Number of complete SIMD chunks remaining.
266    #[inline(always)]
267    pub fn chunks_remaining(&self) -> usize {
268        if self.simd_end > self.pos {
269            (self.simd_end - self.pos) / Arch::LANE_COUNT
270        } else {
271            0
272        }
273    }
274}
275
276impl<'a, 'b, T: Scalar, Arch: SimdArch + SimdKernel<T>, Align: Alignment, Mode: ExecutionMode>
277    Iterator for ZipChunksMut<'a, 'b, T, Arch, Align, Mode>
278{
279    /// Yields `(mutable chunk of A, immutable chunk of B)`.
280    type Item = (
281        SimdView<'a, T, Arch, Align, Mode, &'a mut [T]>,
282        SimdView<'b, T, Arch, Align, Mode, &'b [T]>,
283    );
284
285    #[inline(always)]
286    fn next(&mut self) -> Option<Self::Item> {
287        if self.pos >= self.simd_end {
288            return None;
289        }
290        let lane = Arch::LANE_COUNT;
291        // SAFETY: pos < simd_end <= total <= total_a and total_b; exclusive access
292        // is guaranteed by the `&'a mut [T]` lifetime held by the caller.
293        let (chunk_a, chunk_b) = unsafe {
294            (
295                core::slice::from_raw_parts_mut(self.ptr_a.add(self.pos), lane),
296                core::slice::from_raw_parts(self.ptr_b.add(self.pos), lane),
297            )
298        };
299        self.pos += lane;
300        Some((
301            SimdView::new_mut(chunk_a).expect("ZipChunksMut chunk_a alignment violated"),
302            SimdView::new(chunk_b).expect("ZipChunksMut chunk_b alignment violated"),
303        ))
304    }
305
306    #[inline(always)]
307    fn size_hint(&self) -> (usize, Option<usize>) {
308        let r = self.chunks_remaining();
309        (r, Some(r))
310    }
311}
312
313impl<'a, 'b, T: Scalar, Arch: SimdArch + SimdKernel<T>, Align: Alignment, Mode: ExecutionMode>
314    ExactSizeIterator for ZipChunksMut<'a, 'b, T, Arch, Align, Mode>
315{
316}
317
318impl<'a, 'b, T: Scalar, Arch: SimdArch + SimdKernel<T>, Align: Alignment, Mode: ExecutionMode>
319    core::iter::FusedIterator for ZipChunksMut<'a, 'b, T, Arch, Align, Mode>
320{
321}