hermes_simd_core/view/reduce.rs
1//! Horizontal and pairwise SIMD reductions over [`SimdView`](crate::view::SimdView).
2//!
3//! # Safety
4//!
5//! Every kernel call below is `#[target_feature]`-gated and is therefore sound
6//! only on a host implementing `Arch`. That holds by construction rather than by
7//! inspection: [`SimdView::new`](crate::view::SimdView::new) returns `None` for
8//! an architecture the host cannot execute, and the sparse and copy-on-write
9//! constructors assert the same condition, so possessing one of these
10//! arch-parameterized values *is* the proof. Per-site `SAFETY` comments record
11//! only the obligations that go beyond it — pointer provenance, bounds, and
12//! alignment.
13
14use crate::align::Alignment;
15use crate::arch::SimdArch;
16use crate::execution::ExecutionMode;
17use crate::kernel::SimdKernel;
18use crate::ops::ReductionOp;
19use crate::scalar::Scalar;
20use crate::view::{SimdError, SimdView};
21
22/// Periodic accumulator-flush interval for popcount-style horizontal reductions,
23/// sized by element width to bound intermediate-sum precision loss. 2-byte types
24/// (`f16`/`bf16`/`i16`) have a small exact-integer range (256/2048), so partials
25/// are flushed every 128 chunks; wider types tolerate 32768 chunks per flush.
26#[inline(always)]
27const fn flush_limit_for<T>() -> usize {
28 if core::mem::size_of::<T>() == 2 {
29 128
30 } else {
31 32768
32 }
33}
34
35impl<'a, T: 'a, Arch: SimdArch + SimdKernel<T>, Align: Alignment, Mode: ExecutionMode, Ref: 'a>
36 SimdView<'a, T, Arch, Align, Mode, Ref>
37where
38 T: Scalar,
39{
40 /// Generic SIMD horizontal reduction using a `ReductionOp<T>` strategy ZST.
41 ///
42 /// Processes `UNROLL_FACTOR × LANE_COUNT` elements per iteration using
43 /// `UNROLL_FACTOR` independent accumulators to saturate FMA throughput.
44 ///
45 /// The vector accumulator is initialized to `Op::identity_vector()` — the
46 /// identity element for this reduction (e.g. `+∞` for `Min`, `-∞` for `Max`,
47 /// `0` for `Sum`). This is required for correctness: starting from `Arch::zero()`
48 /// would produce wrong results for `Min`/`Max` on non-negative inputs.
49 ///
50 /// Zero-cost: `_op` is a ZST erased entirely by the compiler.
51 #[inline]
52 pub fn reduce<Op: ReductionOp<T>>(&self, _op: Op) -> T {
53 let data = self.as_slice();
54 let len = data.len();
55 if len == 0 {
56 return Op::identity_scalar();
57 }
58
59 let lane_count = Arch::LANE_COUNT;
60 let unroll_factor = Arch::UNROLL_FACTOR;
61 let chunk_size = lane_count * unroll_factor;
62 let unrolled_len = (len / chunk_size) * chunk_size;
63
64 // SAFETY: `Arch::load_*` is a target-feature kernel (module invariant).
65 // Every caller only ever passes a pointer whose `LANE_COUNT`-element read
66 // stays within `data` (offsets are bounded by `simd_len`/`unrolled_len`),
67 // and the aligned variant is selected only when `Align` proves the base
68 // is arch-aligned.
69 let load = |p: *const T| -> Arch::Vector {
70 if crate::align::is_aligned_for_arch::<Arch, Align>() {
71 unsafe { Arch::load_aligned(p) }
72 } else {
73 unsafe { Arch::load_unaligned(p) }
74 }
75 };
76
77 // SAFETY: the `Op::*` and `identity_vector` calls are target-feature
78 // kernels covered by the module invariant. `unrolled_len` is a multiple
79 // of `chunk_size = LANE_COUNT * UNROLL_FACTOR`, so each `ptr.add(k)` in
80 // the seeds/loop addresses a `LANE_COUNT` window fully within `data`
81 // (`ptr` advances by `chunk_size` per iteration while `i < unrolled_len`).
82 // Initialize with the identity vector so Min/Max start from the correct bound.
83 let mut acc = unsafe { Op::identity_vector::<Arch>() };
84 let mut i = 0usize;
85
86 if unrolled_len >= chunk_size {
87 // Seeds carry the per-element transform (identity for Sum/Min/Max,
88 // abs for AbsSum/AbsMax) — a raw-load seed would skip it for the
89 // first chunk. Cross-accumulator merges use combine_vectors, which
90 // never re-applies the transform to already-transformed partials.
91 let base = data.as_ptr();
92 acc = unsafe {
93 let mut acc0 = Op::transform_vector::<Arch>(load(base));
94 let mut acc1 = Op::transform_vector::<Arch>(load(base.add(lane_count)));
95 let mut acc2 = Op::transform_vector::<Arch>(load(base.add(lane_count * 2)));
96 let mut acc3 = Op::transform_vector::<Arch>(load(base.add(lane_count * 3)));
97 let mut ptr = base.add(chunk_size);
98 i = chunk_size;
99
100 while i < unrolled_len {
101 acc0 = Op::accumulate::<Arch>(acc0, load(ptr));
102 acc1 = Op::accumulate::<Arch>(acc1, load(ptr.add(lane_count)));
103 acc2 = Op::accumulate::<Arch>(acc2, load(ptr.add(lane_count * 2)));
104 acc3 = Op::accumulate::<Arch>(acc3, load(ptr.add(lane_count * 3)));
105 ptr = ptr.add(chunk_size);
106 i += chunk_size;
107 }
108
109 acc0 = Op::combine_vectors::<Arch>(acc0, acc1);
110 acc2 = Op::combine_vectors::<Arch>(acc2, acc3);
111 Op::combine_vectors::<Arch>(acc0, acc2)
112 };
113 }
114
115 // Remaining full SIMD vectors.
116 // SAFETY: `i < simd_len` and `simd_len = (len / LANE_COUNT) * LANE_COUNT`,
117 // so `ptr.add(i)` addresses a `LANE_COUNT` window within `data`;
118 // `Op::accumulate`/`finalize` are target-feature kernels (module invariant).
119 let simd_len = (len / lane_count) * lane_count;
120 let ptr = data.as_ptr();
121 let mut total = unsafe {
122 while i < simd_len {
123 acc = Op::accumulate::<Arch>(acc, load(ptr.add(i)));
124 i += lane_count;
125 }
126 Op::finalize::<Arch>(acc)
127 };
128
129 // Scalar tail — use Op::scalar_accumulate so per-element transforms (e.g. SquaredSum)
130 // apply correctly. For Sum/Min/Max the default delegates to scalar_combine.
131 while i < len {
132 total = Op::scalar_accumulate(total, data[i]);
133 i += 1;
134 }
135
136 total
137 }
138
139 /// Generic pairwise SIMD reduction: `reduce(Op, a ⊗ b)`.
140 ///
141 /// Computes `a[i] * b[i]` lane-wise, then applies `Op::accumulate` and `Op::finalize`.
142 /// For `Op=Dot` this is the standard dot product.
143 ///
144 /// # Errors
145 /// Returns [`SimdError::LengthMismatch`] if slice lengths differ.
146 #[inline]
147 pub fn zip_reduce<Op: ReductionOp<T>, ORef>(
148 &self,
149 other: &SimdView<'_, T, Arch, Align, Mode, ORef>,
150 _op: Op,
151 ) -> Result<T, SimdError>
152 where
153 ORef: 'a,
154 {
155 super::check_lengths_equal(self.len(), other.len())?;
156 let len = self.len();
157 let lane_count = Arch::LANE_COUNT;
158 let unroll_factor = Arch::UNROLL_FACTOR;
159 let chunk_size = lane_count * unroll_factor;
160 let unrolled_len = (len / chunk_size) * chunk_size;
161
162 // SAFETY: identical contract to `reduce`'s `load` — target-feature kernel
163 // (module invariant), and every call passes a pointer whose `LANE_COUNT`
164 // read stays within its slice (offsets bounded by `simd_len`).
165 let load = |p: *const T| -> Arch::Vector {
166 if crate::align::is_aligned_for_arch::<Arch, Align>() {
167 unsafe { Arch::load_aligned(p) }
168 } else {
169 unsafe { Arch::load_unaligned(p) }
170 }
171 };
172
173 let s = self.as_slice();
174 let o = other.as_slice();
175 // SAFETY: target-feature kernels (module invariant). `s` and `o` are
176 // equal length (checked above), and `unrolled_len` is a multiple of
177 // `chunk_size`, so every `pa.add(k)`/`pb.add(k)` addresses a `LANE_COUNT`
178 // window within its slice while `i < unrolled_len`.
179 let mut acc = unsafe { Op::identity_vector::<Arch>() };
180 let mut i = 0usize;
181
182 if unrolled_len >= chunk_size {
183 // Seed the four accumulators with the first pairwise products.
184 // (First chunk cannot use FMA into zero, so we use separate mul.)
185 acc = unsafe {
186 let pair =
187 |pa: *const T, pb: *const T| -> Arch::Vector { Arch::mul(load(pa), load(pb)) };
188 let base_a = s.as_ptr();
189 let base_b = o.as_ptr();
190
191 let mut acc0 = pair(base_a, base_b);
192 let mut acc1 = pair(base_a.add(lane_count), base_b.add(lane_count));
193 let mut acc2 = pair(base_a.add(lane_count * 2), base_b.add(lane_count * 2));
194 let mut acc3 = pair(base_a.add(lane_count * 3), base_b.add(lane_count * 3));
195 let mut pa = base_a.add(chunk_size);
196 let mut pb = base_b.add(chunk_size);
197 i = chunk_size;
198
199 // Main unrolled loop — `fma_pair_accumulate` lets `Dot` emit a
200 // single `vfmadd` instead of a separate `mul` + `add`.
201 while i < unrolled_len {
202 acc0 = Op::fma_pair_accumulate::<Arch>(acc0, load(pa), load(pb));
203 acc1 = Op::fma_pair_accumulate::<Arch>(
204 acc1,
205 load(pa.add(lane_count)),
206 load(pb.add(lane_count)),
207 );
208 acc2 = Op::fma_pair_accumulate::<Arch>(
209 acc2,
210 load(pa.add(lane_count * 2)),
211 load(pb.add(lane_count * 2)),
212 );
213 acc3 = Op::fma_pair_accumulate::<Arch>(
214 acc3,
215 load(pa.add(lane_count * 3)),
216 load(pb.add(lane_count * 3)),
217 );
218 pa = pa.add(chunk_size);
219 pb = pb.add(chunk_size);
220 i += chunk_size;
221 }
222
223 acc0 = Op::accumulate::<Arch>(acc0, acc1);
224 acc2 = Op::accumulate::<Arch>(acc2, acc3);
225 Op::accumulate::<Arch>(acc0, acc2)
226 };
227 }
228
229 // Remaining full SIMD vectors — use `fma_pair_accumulate` here too.
230 // SAFETY: `i < simd_len` bounds each `pa.add(i)`/`pb.add(i)` to a
231 // `LANE_COUNT` window within the equal-length slices; kernels covered by
232 // the module invariant.
233 let simd_len = (len / lane_count) * lane_count;
234 let pa = s.as_ptr();
235 let pb = o.as_ptr();
236 let mut total = unsafe {
237 while i < simd_len {
238 acc = Op::fma_pair_accumulate::<Arch>(acc, load(pa.add(i)), load(pb.add(i)));
239 i += lane_count;
240 }
241 Op::finalize::<Arch>(acc)
242 };
243
244 // Scalar tail — use scalar_combine for correctness with Min/Max.
245 while i < len {
246 total = Op::scalar_combine(total, s[i] * o[i]);
247 i += 1;
248 }
249
250 Ok(total)
251 }
252
253 /// Computes the horizontal sum of population counts of all elements.
254 #[inline]
255 pub fn reduce_popcount(&self) -> usize {
256 let data = self.as_slice();
257 let len = data.len();
258 let lane_count = Arch::LANE_COUNT;
259 let unroll_factor = Arch::UNROLL_FACTOR;
260 let chunk_size = lane_count * unroll_factor;
261 let unrolled_simd_len = (len / chunk_size) * chunk_size;
262 let simd_len = (len / lane_count) * lane_count;
263 let mut total: usize = 0;
264 let mut i = 0usize;
265
266 // SAFETY: `Arch::load_*` is a target-feature kernel (module invariant),
267 // and every call site passes a pointer whose `LANE_COUNT` read stays
268 // within the source slice (offsets bounded by `simd_len`).
269 let load = |p: *const T| -> Arch::Vector {
270 if crate::align::is_aligned_for_arch::<Arch, Align>() {
271 unsafe { Arch::load_aligned(p) }
272 } else {
273 unsafe { Arch::load_unaligned(p) }
274 }
275 };
276
277 let flush_limit = flush_limit_for::<T>();
278
279 // Unrolled loop (4-way register accumulation)
280 if unrolled_simd_len > 0 {
281 let mut acc0 = unsafe { Arch::zero() };
282 let mut acc1 = unsafe { Arch::zero() };
283 let mut acc2 = unsafe { Arch::zero() };
284 let mut acc3 = unsafe { Arch::zero() };
285 let mut count = 0;
286
287 while i < unrolled_simd_len {
288 // SAFETY: `unrolled_simd_len` is a multiple of `chunk_size`, so
289 // `i + lane_count*3 + LANE_COUNT <= unrolled_simd_len <= len`; each
290 // load reads a `LANE_COUNT` window within `data`. Kernels covered
291 // by the module invariant.
292 unsafe {
293 let v0 = load(data.as_ptr().add(i));
294 let v1 = load(data.as_ptr().add(i + lane_count));
295 let v2 = load(data.as_ptr().add(i + lane_count * 2));
296 let v3 = load(data.as_ptr().add(i + lane_count * 3));
297
298 acc0 = Arch::add(acc0, Arch::popcount(v0));
299 acc1 = Arch::add(acc1, Arch::popcount(v1));
300 acc2 = Arch::add(acc2, Arch::popcount(v2));
301 acc3 = Arch::add(acc3, Arch::popcount(v3));
302 }
303 i += chunk_size;
304 count += 1;
305
306 if count == flush_limit {
307 unsafe {
308 let mut acc = Arch::add(acc0, acc1);
309 acc = Arch::add(acc, acc2);
310 acc = Arch::add(acc, acc3);
311 total += Arch::sum_reduce(acc).to_f64() as usize;
312 acc0 = Arch::zero();
313 acc1 = Arch::zero();
314 acc2 = Arch::zero();
315 acc3 = Arch::zero();
316 }
317 count = 0;
318 }
319 }
320
321 unsafe {
322 let mut acc = Arch::add(acc0, acc1);
323 acc = Arch::add(acc, acc2);
324 acc = Arch::add(acc, acc3);
325 total += Arch::sum_reduce(acc).to_f64() as usize;
326 }
327 }
328
329 // Middle loop (single register accumulation)
330 if i < simd_len {
331 let mut acc = unsafe { Arch::zero() };
332 while i < simd_len {
333 // SAFETY: `i < simd_len = (len / LANE_COUNT) * LANE_COUNT`, so the
334 // load reads a `LANE_COUNT` window within `data`.
335 unsafe {
336 let v = load(data.as_ptr().add(i));
337 acc = Arch::add(acc, Arch::popcount(v));
338 }
339 i += lane_count;
340 }
341 total += unsafe { Arch::sum_reduce(acc) }.to_f64() as usize;
342 }
343
344 // Scalar tail loop
345 while i < len {
346 total += data[i].count_ones() as usize;
347 i += 1;
348 }
349
350 total
351 }
352
353 /// Horizontal sum of population counts of `op(self[i], other[i])` for a
354 /// bitwise [`ElementOp`] (`BitAnd`/`BitOr`/`BitXor`).
355 ///
356 /// One generic 4-accumulator popcount reduction shared by
357 /// [`reduce_popcount_and`](Self::reduce_popcount_and),
358 /// [`reduce_popcount_or`](Self::reduce_popcount_or) and
359 /// [`reduce_popcount_xor`](Self::reduce_popcount_xor). The combining op is a
360 /// ZST monomorphized away, so each wrapper compiles to exactly the code its
361 /// former hand-written body did — the three ~100-line bodies collapse to one.
362 ///
363 /// # Errors
364 /// Returns [`SimdError::LengthMismatch`] if slice lengths differ.
365 #[inline]
366 fn reduce_popcount_op<ORef, Op>(
367 &self,
368 other: &SimdView<'_, T, Arch, Align, Mode, ORef>,
369 op: Op,
370 ) -> Result<usize, SimdError>
371 where
372 ORef: 'a,
373 Op: crate::ops::ElementOp<T>,
374 {
375 super::check_lengths_equal(self.len(), other.len())?;
376 let s = self.as_slice();
377 let o = other.as_slice();
378 let len = s.len();
379 let lane_count = Arch::LANE_COUNT;
380 let unroll_factor = Arch::UNROLL_FACTOR;
381 let chunk_size = lane_count * unroll_factor;
382 let unrolled_simd_len = (len / chunk_size) * chunk_size;
383 let simd_len = (len / lane_count) * lane_count;
384 let mut total: usize = 0;
385 let mut i = 0usize;
386
387 // SAFETY: `Arch::load_*` is a target-feature kernel (module invariant),
388 // and every call site passes a pointer whose `LANE_COUNT` read stays
389 // within the source slice (offsets bounded by `simd_len`).
390 let load = |p: *const T| -> Arch::Vector {
391 if crate::align::is_aligned_for_arch::<Arch, Align>() {
392 unsafe { Arch::load_aligned(p) }
393 } else {
394 unsafe { Arch::load_unaligned(p) }
395 }
396 };
397
398 let flush_limit = flush_limit_for::<T>();
399
400 if unrolled_simd_len > 0 {
401 let mut acc0 = unsafe { Arch::zero() };
402 let mut acc1 = unsafe { Arch::zero() };
403 let mut acc2 = unsafe { Arch::zero() };
404 let mut acc3 = unsafe { Arch::zero() };
405 let mut count = 0;
406
407 while i < unrolled_simd_len {
408 // SAFETY: `unrolled_simd_len` is a multiple of `chunk_size` and
409 // `s`/`o` are equal length, so `i + lane_count*3 + LANE_COUNT`
410 // stays within both slices. Kernels covered by the module invariant.
411 unsafe {
412 let va0 = load(s.as_ptr().add(i));
413 let vb0 = load(o.as_ptr().add(i));
414 let va1 = load(s.as_ptr().add(i + lane_count));
415 let vb1 = load(o.as_ptr().add(i + lane_count));
416 let va2 = load(s.as_ptr().add(i + lane_count * 2));
417 let vb2 = load(o.as_ptr().add(i + lane_count * 2));
418 let va3 = load(s.as_ptr().add(i + lane_count * 3));
419 let vb3 = load(o.as_ptr().add(i + lane_count * 3));
420
421 acc0 = Arch::add(acc0, Arch::popcount(op.apply::<Arch>(va0, vb0)));
422 acc1 = Arch::add(acc1, Arch::popcount(op.apply::<Arch>(va1, vb1)));
423 acc2 = Arch::add(acc2, Arch::popcount(op.apply::<Arch>(va2, vb2)));
424 acc3 = Arch::add(acc3, Arch::popcount(op.apply::<Arch>(va3, vb3)));
425 }
426 i += chunk_size;
427 count += 1;
428
429 if count == flush_limit {
430 unsafe {
431 let mut acc = Arch::add(acc0, acc1);
432 acc = Arch::add(acc, acc2);
433 acc = Arch::add(acc, acc3);
434 total += Arch::sum_reduce(acc).to_f64() as usize;
435 acc0 = Arch::zero();
436 acc1 = Arch::zero();
437 acc2 = Arch::zero();
438 acc3 = Arch::zero();
439 }
440 count = 0;
441 }
442 }
443
444 unsafe {
445 let mut acc = Arch::add(acc0, acc1);
446 acc = Arch::add(acc, acc2);
447 acc = Arch::add(acc, acc3);
448 total += Arch::sum_reduce(acc).to_f64() as usize;
449 }
450 }
451
452 if i < simd_len {
453 let mut acc = unsafe { Arch::zero() };
454 while i < simd_len {
455 // SAFETY: `i < simd_len` bounds both `s.add(i)`/`o.add(i)` loads
456 // to a `LANE_COUNT` window within the equal-length slices.
457 unsafe {
458 let va = load(s.as_ptr().add(i));
459 let vb = load(o.as_ptr().add(i));
460 acc = Arch::add(acc, Arch::popcount(op.apply::<Arch>(va, vb)));
461 }
462 i += lane_count;
463 }
464 total += unsafe { Arch::sum_reduce(acc) }.to_f64() as usize;
465 }
466
467 while i < len {
468 total += op.apply_scalar(s[i], o[i]).count_ones() as usize;
469 i += 1;
470 }
471
472 Ok(total)
473 }
474
475 /// Computes the horizontal sum of population counts of `self[i] & other[i]`.
476 ///
477 /// # Errors
478 /// Returns [`SimdError::LengthMismatch`] if slice lengths differ.
479 #[inline]
480 pub fn reduce_popcount_and<ORef>(
481 &self,
482 other: &SimdView<'_, T, Arch, Align, Mode, ORef>,
483 ) -> Result<usize, SimdError>
484 where
485 ORef: 'a,
486 {
487 self.reduce_popcount_op(other, crate::ops::BitAnd)
488 }
489
490 /// Computes the horizontal sum of population counts of `self[i] | other[i]`.
491 ///
492 /// # Errors
493 /// Returns [`SimdError::LengthMismatch`] if slice lengths differ.
494 #[inline]
495 pub fn reduce_popcount_or<ORef>(
496 &self,
497 other: &SimdView<'_, T, Arch, Align, Mode, ORef>,
498 ) -> Result<usize, SimdError>
499 where
500 ORef: 'a,
501 {
502 self.reduce_popcount_op(other, crate::ops::BitOr)
503 }
504
505 /// Computes the horizontal sum of population counts of `self[i] ^ other[i]`.
506 ///
507 /// # Errors
508 /// Returns [`SimdError::LengthMismatch`] if slice lengths differ.
509 #[inline]
510 pub fn reduce_popcount_xor<ORef>(
511 &self,
512 other: &SimdView<'_, T, Arch, Align, Mode, ORef>,
513 ) -> Result<usize, SimdError>
514 where
515 ORef: 'a,
516 {
517 self.reduce_popcount_op(other, crate::ops::BitXor)
518 }
519}
520
521impl<
522 'a,
523 T: 'a,
524 Arch: crate::arch::SimdArch + crate::kernel::SimdKernel<T>,
525 Align: crate::align::Alignment,
526 Mode: crate::execution::ExecutionMode,
527 Ref: 'a,
528 > SimdView<'a, T, Arch, Align, Mode, Ref>
529where
530 T: crate::scalar::Scalar + crate::scalar::NumericElement,
531{
532 /// Returns `Some((index, value))` for the first minimum element.
533 ///
534 /// Correctness: a SIMD reduction pass finds the minimum value, then one
535 /// validation scan rejects NaNs while retaining its first occurrence.
536 ///
537 /// Returns `None` for an empty slice or when any element is NaN. The
538 /// validation scan rejects the whole unordered domain, so an intermediate
539 /// backend result never escapes. Equal extrema use the first slice element,
540 /// including its signed-zero representation.
541 #[inline]
542 pub fn argmin(&self) -> Option<(usize, T)> {
543 let data = self.as_slice();
544 if data.is_empty() {
545 return None;
546 }
547 let min_val = self.reduce(crate::ops::Min);
548 Self::locate_ordered_extremum(data, min_val)
549 }
550
551 /// Returns `Some((index, value))` for the first maximum element.
552 ///
553 /// Correctness: a SIMD reduction pass finds the maximum value, then one
554 /// validation scan rejects NaNs while retaining its first occurrence.
555 ///
556 /// Returns `None` for an empty slice or when any element is NaN. The
557 /// validation scan rejects the whole unordered domain, so an intermediate
558 /// backend result never escapes. Equal extrema use the first slice element,
559 /// including its signed-zero representation.
560 #[inline]
561 pub fn argmax(&self) -> Option<(usize, T)> {
562 let data = self.as_slice();
563 if data.is_empty() {
564 return None;
565 }
566 let max_val = self.reduce(crate::ops::Max);
567 Self::locate_ordered_extremum(data, max_val)
568 }
569
570 #[inline]
571 fn locate_ordered_extremum(data: &[T], extremum: T) -> Option<(usize, T)> {
572 let lane_count = Arch::LANE_COUNT;
573 // Shift-based construction avoids the `1 << 64` overflow a 64-lane
574 // backend would hit; `lane_count` never exceeds `u64::BITS`.
575 let lane_mask = u64::MAX >> (u64::BITS as usize - lane_count.min(64));
576 let vector_len = (data.len() / lane_count) * lane_count;
577 let mut first: Option<usize> = None;
578 let mut index = 0usize;
579
580 while index < vector_len {
581 // SAFETY: `index <= vector_len - lane_count`, so the load reads
582 // exactly `lane_count` in-bounds elements of `data`; the aligned
583 // variant is selected only when `Align` guarantees the view's base
584 // pointer is arch-aligned, and `index` is a multiple of `lane_count`.
585 // Constructing `Arch` already asserts its target features.
586 let (ordered, hits) = unsafe {
587 let ptr = data.as_ptr().add(index);
588 let v = if crate::align::is_aligned_for_arch::<Arch, Align>() {
589 Arch::load_aligned(ptr)
590 } else {
591 Arch::load_unaligned(ptr)
592 };
593 // `x == x` is false exactly for NaN, so a lane absent from
594 // `ordered` marks a NaN.
595 let ordered =
596 Arch::mask_to_bitmask(Arch::vector_to_mask(Arch::cmp_eq(v, v))) & lane_mask;
597 let hits = if first.is_none() {
598 let target = Arch::splat(extremum);
599 Arch::mask_to_bitmask(Arch::vector_to_mask(Arch::cmp_eq(v, target))) & lane_mask
600 } else {
601 0
602 };
603 (ordered, hits)
604 };
605
606 if ordered != lane_mask {
607 return None;
608 }
609 if hits != 0 {
610 first = Some(index + hits.trailing_zeros() as usize);
611 }
612 index += lane_count;
613 }
614
615 for (offset, value) in data[index..].iter().copied().enumerate() {
616 if value.is_nan() {
617 return None;
618 }
619 if first.is_none() && value.partial_cmp(&extremum) == Some(core::cmp::Ordering::Equal) {
620 first = Some(index + offset);
621 }
622 }
623
624 // Report the stored element rather than the reduced extremum so equal
625 // values keep their own representation, notably signed zero.
626 first.map(|at| (at, data[at]))
627 }
628}