Skip to main content

wide/
u64x4_.rs

1use super::*;
2
3pick! {
4  if #[cfg(target_feature="avx2")] {
5    /// A SIMD vector with four elements of type [`u64`].
6    ///
7    /// See the [crate level documentation] for more information about SIMD
8    /// vectors.
9    ///
10    /// [crate level documentation]: crate
11    #[derive(Default, Clone, Copy, PartialEq, Eq)]
12    #[repr(C, align(32))]
13    pub struct u64x4 { pub(crate) avx2: m256i }
14  } else {
15    /// A SIMD vector with four elements of type [`u64`].
16    ///
17    /// See the [crate level documentation] for more information about SIMD
18    /// vectors.
19    ///
20    /// [crate level documentation]: crate
21    #[derive(Default, Clone, Copy, PartialEq, Eq)]
22    #[repr(C, align(32))]
23    pub struct u64x4 { pub(crate) a : u64x2, pub(crate) b : u64x2 }
24  }
25}
26
27impl_simd_uint! {
28  unsafe {
29    T = u64,
30    N = 4,
31    Simd = u64x4,
32    IntSimd = i64x4,
33    T_BITS = 64,
34    T_BITS_MUL_2 = 128,
35    [0, 1, 2, 3],
36    optional_type_x86_inner { X86Inner = __m256i },
37    optional_type_arm_inner {},
38    optional_type_wasm_inner {},
39  }
40
41  #[inline]
42  fn not(self) -> Self {
43    pick! {
44      if #[cfg(target_feature="avx2")] {
45        Self { avx2: self.avx2.not()  }
46      } else {
47        Self {
48          a : self.a.not(),
49          b : self.b.not(),
50        }
51      }
52    }
53  }
54
55  #[inline]
56  fn add(self, rhs: Self) -> Self::Output {
57    pick! {
58      if #[cfg(target_feature="avx2")] {
59        Self { avx2: add_i64_m256i(self.avx2, rhs.avx2) }
60      } else {
61        Self {
62          a : self.a.add(rhs.a),
63          b : self.b.add(rhs.b),
64        }
65      }
66    }
67  }
68
69  #[inline]
70  fn sub(self, rhs: Self) -> Self::Output {
71    pick! {
72      if #[cfg(target_feature="avx2")] {
73        Self { avx2: sub_i64_m256i(self.avx2, rhs.avx2) }
74      } else {
75        Self {
76          a : self.a.sub(rhs.a),
77          b : self.b.sub(rhs.b),
78        }
79      }
80    }
81  }
82
83  #[inline]
84  fn mul(self, rhs: Self) -> Self::Output {
85    pick! {
86      if #[cfg(target_feature="avx2")] {
87        let arr1: [i64; 4] = cast(self);
88        let arr2: [i64; 4] = cast(rhs);
89        cast([
90          arr1[0].wrapping_mul(arr2[0]),
91          arr1[1].wrapping_mul(arr2[1]),
92          arr1[2].wrapping_mul(arr2[2]),
93          arr1[3].wrapping_mul(arr2[3]),
94        ])
95      } else {
96        Self { a: self.a.mul(rhs.a), b: self.b.mul(rhs.b) }
97      }
98    }
99  }
100
101  #[inline]
102  fn bitand(self, rhs: Self) -> Self::Output {
103    pick! {
104      if #[cfg(target_feature="avx2")] {
105        Self { avx2: bitand_m256i(self.avx2, rhs.avx2) }
106      } else {
107        Self {
108          a : self.a.bitand(rhs.a),
109          b : self.b.bitand(rhs.b),
110        }
111      }
112    }
113  }
114
115  #[inline]
116  fn bitor(self, rhs: Self) -> Self::Output {
117    pick! {
118    if #[cfg(target_feature="avx2")] {
119        Self { avx2: bitor_m256i(self.avx2, rhs.avx2) }
120      } else {
121        Self {
122          a : self.a.bitor(rhs.a),
123          b : self.b.bitor(rhs.b),
124        }
125      }
126    }
127  }
128
129  #[inline]
130  fn bitxor(self, rhs: Self) -> Self::Output {
131    pick! {
132      if #[cfg(target_feature="avx2")] {
133        Self { avx2: bitxor_m256i(self.avx2, rhs.avx2) }
134      } else {
135        Self {
136          a : self.a.bitxor(rhs.a),
137          b : self.b.bitxor(rhs.b),
138        }
139      }
140    }
141  }
142
143  #[inline]
144  fn simd_eq(self, rhs: Self) -> Self::Output {
145    pick! {
146      if #[cfg(target_feature="avx2")] {
147        Self { avx2: cmp_eq_mask_i64_m256i(self.avx2, rhs.avx2) }
148      } else {
149        Self {
150          a : self.a.simd_eq(rhs.a),
151          b : self.b.simd_eq(rhs.b),
152        }
153      }
154    }
155  }
156
157  #[inline]
158  fn simd_ne(self, rhs: Self) -> Self::Output {
159    pick! {
160      if #[cfg(target_feature="avx2")] {
161        !self.simd_eq(rhs)
162      } else {
163        Self {
164          a : self.a.simd_ne(rhs.a),
165          b : self.b.simd_ne(rhs.b),
166        }
167      }
168    }
169  }
170
171  #[inline]
172  fn simd_lt(self, rhs: Self) -> Self::Output {
173    // lt is just gt the other way around
174    rhs.simd_gt(self)
175  }
176
177  #[inline]
178  fn simd_gt(self, rhs: Self) -> Self::Output {
179    pick! {
180      if #[cfg(target_feature="avx2")] {
181        // no unsigned gt than so inverting the high bit will get the correct result
182        let highbit = u64x4::splat(1 << 63);
183        Self { avx2: cmp_gt_mask_i64_m256i((self ^ highbit).avx2, (rhs ^ highbit).avx2) }
184      } else {
185        Self {
186          a : self.a.simd_gt(rhs.a),
187          b : self.b.simd_gt(rhs.b),
188        }
189      }
190    }
191  }
192
193  #[inline]
194  fn simd_le(self, rhs: Self) -> Self::Output {
195    pick! {
196      if #[cfg(target_feature="avx2")] {
197        !self.simd_gt(rhs)
198      } else {
199        Self {
200          a : self.a.simd_le(rhs.a),
201          b : self.b.simd_le(rhs.b),
202        }
203      }
204    }
205  }
206
207  #[inline]
208  fn simd_ge(self, rhs: Self) -> Self::Output {
209    pick! {
210      if #[cfg(target_feature="avx2")] {
211        !self.simd_lt(rhs)
212      } else {
213        Self {
214          a : self.a.simd_ge(rhs.a),
215          b : self.b.simd_ge(rhs.b),
216        }
217      }
218    }
219  }
220
221  #[inline]
222  pub fn reduce_add(self) -> u64 {
223    pick! {
224      if #[cfg(all(target_arch="x86_64", target_feature="avx2"))] {
225        let zwxx  = shuffle_ai_i64_all_m256i::<0b00_00_11_10>(self.avx2);
226        let xz_yw = add_i64_m256i(zwxx, self.avx2);
227        let yw_xz  = shuffle_ai_i64_all_m256i::<0b00_00_00_01>(xz_yw);
228        let sum = add_i64_m256i(xz_yw, yw_xz);
229        extract_i64_from_m256i::<0>(sum).cast_unsigned()
230      } else {
231        let array: [u64; 4] = cast(self);
232        array[0]
233          .wrapping_add(array[1])
234          .wrapping_add(array[2])
235          .wrapping_add(array[3])
236      }
237    }
238  }
239
240  #[inline]
241  pub fn reduce_mul(self) -> u64 {
242    let array: [u64; 4] = cast(self);
243    array[0]
244      .wrapping_mul(array[1])
245      .wrapping_mul(array[2])
246      .wrapping_mul(array[3])
247  }
248
249  #[inline]
250  pub fn bitselect(self, if_one: Self, if_zero: Self) -> Self {
251    pick! {
252      if #[cfg(target_feature="avx2")] {
253        Self {
254          avx2: bitor_m256i(
255            bitand_m256i(if_one.avx2, self.avx2),
256            bitandnot_m256i(self.avx2, if_zero.avx2),
257          ),
258        }
259      } else {
260        Self {
261          a: self.a.bitselect(if_one.a, if_zero.a),
262          b: self.b.bitselect(if_one.b, if_zero.b),
263        }
264      }
265    }
266  }
267
268  #[inline]
269  fn select(self, if_true: Self, if_false: Self) -> Self {
270    pick! {
271      if #[cfg(target_feature="avx2")] {
272        Self { avx2: blend_varying_i8_m256i(if_false.avx2,if_true.avx2,self.avx2) }
273      } else {
274        Self {
275          a : self.a.select(if_true.a, if_false.a),
276          b : self.b.select(if_true.b, if_false.b),
277        }
278      }
279    }
280  }
281
282  #[inline]
283  pub fn to_bitmask(self) -> u32 {
284    pick! {
285      if #[cfg(target_feature="avx2")] {
286        // use f64 move_mask since it is the same size as i64
287        move_mask_m256d(cast(self.avx2)) as u32
288      } else {
289        self.a.to_bitmask() | (self.b.to_bitmask() << 2)
290      }
291    }
292  }
293
294  #[inline]
295  pub fn any(self) -> bool {
296    pick! {
297      if #[cfg(target_feature="avx2")] {
298        move_mask_m256d(cast(self.avx2)) != 0
299      } else {
300        (self.a | self.b).any()
301      }
302    }
303  }
304
305  #[inline]
306  pub fn all(self) -> bool {
307    pick! {
308      if #[cfg(target_feature="avx2")] {
309        move_mask_m256d(cast(self.avx2)) == 0b1111
310      } else {
311        (self.a & self.b).all()
312      }
313    }
314  }
315
316  #[inline]
317  pub fn shuffle(self, indices: u64x4) -> Self {
318    pick! {
319      if #[cfg(all(target_feature="avx512f", target_feature="avx512vl"))] {
320        #[cfg(target_arch = "x86")]
321        use core::arch::x86::_mm256_permutexvar_epi64;
322        #[cfg(target_arch = "x86_64")]
323        use core::arch::x86_64::_mm256_permutexvar_epi64;
324        // TODO(safe_arch): Add `_mm256_permutexvar_epi64`.
325        Self { avx2: m256i(unsafe { _mm256_permutexvar_epi64(indices.avx2.0, self.avx2.0) }) }
326      } else if #[cfg(any(
327        target_feature = "ssse3",
328        all(target_arch = "aarch64", target_feature = "neon"),
329        target_feature = "simd128",
330      ))] {
331        let self_bytes = cast::<u64x4, u8x32>(self);
332        let byte_indices = indices.to_byte_indices();
333
334        cast::<u8x32, u64x4>(self_bytes.shuffle(byte_indices))
335      } else {
336        let self_array = self.to_array();
337        let indices_array = indices.to_array();
338
339        let mut result = [0; 4];
340        for i in 0..4 {
341          let index = indices_array[i] as usize;
342          if index < 4 {
343            result[i] = self_array[index];
344          }
345        }
346
347        Self::new(result)
348      }
349    }
350  }
351
352  #[inline]
353  pub fn shuffle_zeroing(self, indices: u64x4) -> Self {
354    pick! {
355      if #[cfg(any(
356        target_feature = "ssse3",
357        all(target_arch = "aarch64", target_feature = "neon"),
358        target_feature = "simd128",
359      ))] {
360        // Even if the `u8x32::shuffle` implementation is zeroing, our 64-bit to
361        // 8-bit can trigger an overflow causing incorrect behavior
362        self.shuffle(indices) & indices.simd_lt(4)
363      } else {
364        // The fallback branch of `shuffle` already has the behavior we want
365        self.shuffle(indices)
366      }
367    }
368  }
369
370  #[inline]
371  pub fn shuffle_wrapping(self, indices: u64x4) -> Self {
372    pick! {
373      if #[cfg(all(target_feature = "avx512f", target_feature = "avx512vl"))] {
374        // `avx512` shuffle intrinsics are wrapping
375        self.shuffle(indices)
376      } else {
377        self.shuffle(indices & 3)
378      }
379    }
380  }
381
382  #[inline]
383  fn shuffle(self: [u64x4; 2], indices: u64x4) -> u64x4 {
384    pick! {
385      if #[cfg(all(target_feature = "avx512f", target_feature = "avx512vl"))] {
386        u64x4 { avx2: shuffle_abv_i64_all_m256i(self[0].avx2, indices.avx2, self[1].avx2) }
387      } else {
388        let self_bytes = cast::<[u64x4; 2], [u8x32; 2]>(self);
389        let byte_indices = indices.to_byte_indices();
390
391        cast::<u8x32, u64x4>(self_bytes.shuffle(byte_indices))
392      }
393    }
394  }
395
396  #[inline]
397  fn shuffle_zeroing(self: [u64x4; 2], indices: u64x4) -> u64x4 {
398    // Even if the `u8x32::shuffle` implementation is zeroing, our 32-bit to
399    // 8-bit can trigger an overflow causing incorrect behavior
400    self.shuffle(indices) & indices.simd_lt(8)
401  }
402
403  #[inline]
404  fn shuffle_wrapping(self: [u64x4; 2], indices: u64x4) -> u64x4 {
405    pick! {
406      if #[cfg(all(target_feature = "avx512f", target_feature = "avx512vl"))] {
407        // `avx512` shuffle intrinsics are wrapping
408        self.shuffle(indices)
409      } else {
410        self.shuffle(indices & 7)
411      }
412    }
413  }
414
415  #[inline]
416  fn shuffle(self: [u64x4; 3], indices: u64x4) -> u64x4 {
417    let self_bytes = cast::<[u64x4; 3], [u8x32; 3]>(self);
418    let byte_indices = indices.to_byte_indices();
419
420    cast::<u8x32, u64x4>(self_bytes.shuffle(byte_indices))
421  }
422
423  #[inline]
424  fn shuffle_zeroing(self: [u64x4; 3], indices: u64x4) -> u64x4 {
425    // Even if the `u8x32::shuffle` implementation is zeroing, our 32-bit to
426    // 8-bit can trigger an overflow causing incorrect behavior
427    self.shuffle(indices) & indices.simd_lt(12)
428  }
429
430  #[inline]
431  fn shuffle_wrapping(self: [u64x4; 3], indices: u64x4) -> u64x4 {
432    self.shuffle(indices % 12)
433  }
434
435  #[inline]
436  fn shuffle(self: [u64x4; 4], indices: u64x4) -> u64x4 {
437    let self_bytes = cast::<[u64x4; 4], [u8x32; 4]>(self);
438    let byte_indices = indices.to_byte_indices();
439
440    cast::<u8x32, u64x4>(self_bytes.shuffle(byte_indices))
441  }
442
443  #[inline]
444  fn shuffle_zeroing(self: [u64x4; 4], indices: u64x4) -> u64x4 {
445    // Even if the `u8x32::shuffle` implementation is zeroing, our 64-bit to
446    // 8-bit can trigger an overflow causing incorrect behavior
447    self.shuffle(indices) & indices.simd_lt(16)
448  }
449
450  #[inline]
451  fn shuffle_wrapping(self: [u64x4; 4], indices: u64x4) -> u64x4 {
452    self.shuffle(indices & 15)
453  }
454
455  ///
456  /// Currently this function is only accelerated on `avx2`.
457  #[inline]
458  pub fn transpose(data: [Self; 4]) -> [Self; 4] {
459    pick! {
460      if #[cfg(target_feature="avx2")] {
461        // Can this be optimized?
462        // TODO: Once unpack functions are added, remove these casts
463        let a = data[0].cast_signed().unpack_lo(data[2].cast_signed());
464        let b = data[1].cast_signed().unpack_lo(data[3].cast_signed());
465        let c = data[0].cast_signed().unpack_hi(data[2].cast_signed());
466        let d = data[1].cast_signed().unpack_hi(data[3].cast_signed());
467        [
468          a.unpack_lo(b).cast_unsigned(),
469          a.unpack_hi(b).cast_unsigned(),
470          c.unpack_lo(d).cast_unsigned(),
471          c.unpack_hi(d).cast_unsigned(),
472        ]
473      } else {
474        #[inline(always)]
475        fn transpose_column(data: &[u64x4; 4], index: usize) -> u64x4 {
476          u64x4::new([
477            data[0].as_array()[index],
478            data[1].as_array()[index],
479            data[2].as_array()[index],
480            data[3].as_array()[index],
481          ])
482        }
483
484        [
485          transpose_column(&data, 0),
486          transpose_column(&data, 1),
487          transpose_column(&data, 2),
488          transpose_column(&data, 3),
489        ]
490      }
491    }
492  }
493
494  #[inline]
495  fn shl(self, rhs: Self) -> Self::Output {
496    pick! {
497      if #[cfg(target_feature="avx2")] {
498        // mask the shift count to 63 to have same behavior on all platforms
499        let shift_by = rhs & Self::splat(63);
500        Self { avx2: shl_each_u64_m256i(self.avx2, shift_by.avx2) }
501      } else {
502        Self {
503          a : self.a.shl(rhs.a),
504          b : self.b.shl(rhs.b),
505        }
506      }
507    }
508  }
509
510  #[inline]
511  fn shl(self, rhs: u32) -> Self::Output {
512    pick! {
513      if #[cfg(target_feature="avx2")] {
514        // Use `rhs % 64` to perform wrapping shift and not unbounded shift.
515        #[expect(clippy::suspicious_arithmetic_impl)]
516        let shift = cast([rhs as u64 & 63, 0]);
517        Self { avx2: shl_all_u64_m256i(self.avx2, shift) }
518      } else {
519        Self {
520          a : self.a.shl(rhs),
521          b : self.b.shl(rhs),
522        }
523      }
524    }
525  }
526
527  #[inline]
528  fn shr(self, rhs: Self) -> Self::Output {
529    pick! {
530      if #[cfg(target_feature="avx2")] {
531        // mask the shift count to 63 to have same behavior on all platforms
532        let shift_by = rhs & Self::splat(63);
533        Self { avx2: shr_each_u64_m256i(self.avx2, shift_by.avx2) }
534      } else {
535        Self {
536          a : self.a.shr(rhs.a),
537          b : self.b.shr(rhs.b),
538        }
539      }
540    }
541  }
542
543  #[inline]
544  fn shr(self, rhs: u32) -> Self::Output {
545    pick! {
546      if #[cfg(target_feature="avx2")] {
547        // Use `rhs % 64` to perform wrapping shift and not unbounded shift.
548        #[expect(clippy::suspicious_arithmetic_impl)]
549        let shift = cast([rhs as u64 & 63, 0]);
550        Self { avx2: shr_all_u64_m256i(self.avx2, shift) }
551      } else {
552        Self {
553          a : self.a.shr(rhs),
554          b : self.b.shr(rhs),
555        }
556      }
557    }
558  }
559
560  #[inline]
561  pub fn max(self, rhs: Self) -> Self {
562    self.simd_gt(rhs).select(self, rhs)
563  }
564
565  #[inline]
566  pub fn min(self, rhs: Self) -> Self {
567    self.simd_lt(rhs).select(self, rhs)
568  }
569
570  #[inline]
571  pub fn reduce_max(self) -> u64 {
572    let array: [u64; 4] = cast(self);
573    array[0].max(array[1]).max(array[2]).max(array[3])
574  }
575
576  #[inline]
577  pub fn reduce_min(self) -> u64 {
578    let array: [u64; 4] = cast(self);
579    array[0].min(array[1]).min(array[2]).min(array[3])
580  }
581
582  #[inline]
583  pub fn unbounded_shl(self, rhs: Self) -> Self {
584    pick! {
585      if #[cfg(target_feature="avx2")] {
586        Self { avx2: shl_each_u64_m256i(self.avx2, rhs.avx2) }
587      } else {
588        Self {
589          a: self.a.unbounded_shl(rhs.a),
590          b: self.b.unbounded_shl(rhs.b),
591        }
592      }
593    }
594  }
595
596  #[inline]
597  pub fn unbounded_shl_scalar(self, rhs: u32) -> Self {
598    pick! {
599      if #[cfg(target_feature="avx2")] {
600        Self { avx2: shl_all_u64_m256i(self.avx2, cast([rhs as u64, 0])) }
601      } else {
602        Self {
603          a: self.a.unbounded_shl_scalar(rhs),
604          b: self.b.unbounded_shl_scalar(rhs),
605        }
606      }
607    }
608  }
609
610  #[inline]
611  pub fn unbounded_shr(self, rhs: Self) -> Self {
612    pick! {
613      if #[cfg(target_feature="avx2")] {
614        Self { avx2: shr_each_u64_m256i(self.avx2, rhs.avx2) }
615      } else {
616        Self {
617          a: self.a.unbounded_shr(rhs.a),
618          b: self.b.unbounded_shr(rhs.b),
619        }
620      }
621    }
622  }
623
624  #[inline]
625  pub fn unbounded_shr_scalar(self, rhs: u32) -> Self {
626    pick! {
627      if #[cfg(target_feature="avx2")] {
628        Self { avx2: shr_all_u64_m256i(self.avx2, cast([rhs as u64, 0])) }
629      } else {
630        Self {
631          a: self.a.unbounded_shr_scalar(rhs),
632          b: self.b.unbounded_shr_scalar(rhs),
633        }
634      }
635    }
636  }
637
638  #[inline]
639  pub fn saturating_add(self, rhs: Self) -> Self {
640    pick! {
641      if #[cfg(target_feature="avx2")] {
642        let result = self + rhs;
643        let overflow = result.simd_lt(self);
644        // Return `MAX` (all bits set) if overflow occurs.
645        result | overflow
646      } else {
647        Self {
648          a: self.a.saturating_add(rhs.a),
649          b: self.b.saturating_add(rhs.b),
650        }
651      }
652    }
653  }
654
655  #[inline]
656  pub fn saturating_sub(self, rhs: Self) -> Self {
657    pick! {
658      if #[cfg(target_feature="avx2")] {
659        let result = self - rhs;
660        let no_overflow = result.simd_le(self);
661        // Return `0` (no bits set) if overflow occurs.
662        result & no_overflow
663      } else {
664        Self {
665          a: self.a.saturating_sub(rhs.a),
666          b: self.b.saturating_sub(rhs.b),
667        }
668      }
669    }
670  }
671
672  #[inline]
673  pub fn overflowing_mul(self, rhs: Self) -> (Self, Self) {
674    // TODO(perf): This implementation looks quite bad. Is there a better
675    // one? This intentionally avoids `mul_keep_low_high` because getting the
676    // high bits of 64-bit multiplication could be slow.
677
678    let self_array = self.to_array();
679    let rhs_array = rhs.to_array();
680
681    let result = [
682      self_array[0].overflowing_mul(rhs_array[0]),
683      self_array[1].overflowing_mul(rhs_array[1]),
684      self_array[2].overflowing_mul(rhs_array[2]),
685      self_array[3].overflowing_mul(rhs_array[3]),
686    ];
687    (
688      Self::new([result[0].0, result[1].0, result[2].0, result[3].0]),
689      Self::new([
690        -(result[0].1 as i64) as u64,
691        -(result[1].1 as i64) as u64,
692        -(result[2].1 as i64) as u64,
693        -(result[3].1 as i64) as u64,
694      ]),
695    )
696  }
697
698  optional_fn_widening_mul {
699    // Cannot have `widening_mul` because there is no `u128x4` type.
700  }
701
702  #[inline]
703  pub fn mul_keep_low_high(self, rhs: Self) -> (Self, Self) {
704    // TODO(perf): This implementation looks quite bad. Is there a better
705    // one?
706
707    let self_array = self.to_array();
708    let rhs_array = rhs.to_array();
709
710    let widening_mul = [
711      (self_array[0] as u128).wrapping_mul(rhs_array[0] as u128),
712      (self_array[1] as u128).wrapping_mul(rhs_array[1] as u128),
713      (self_array[2] as u128).wrapping_mul(rhs_array[2] as u128),
714      (self_array[3] as u128).wrapping_mul(rhs_array[3] as u128),
715    ];
716
717    (
718      Self::new([
719        widening_mul[0] as u64,
720        widening_mul[1] as u64,
721        widening_mul[2] as u64,
722        widening_mul[3] as u64,
723      ]),
724      Self::new([
725        (widening_mul[0] >> 64) as u64,
726        (widening_mul[1] >> 64) as u64,
727        (widening_mul[2] >> 64) as u64,
728        (widening_mul[3] >> 64) as u64,
729      ]),
730    )
731  }
732
733  #[inline]
734  pub fn mul_keep_high(self, rhs: Self) -> Self {
735    pick! {
736      if #[cfg(target_feature="avx2")] {
737        let arr1: [u64; 4] = cast(self);
738        let arr2: [u64; 4] = cast(rhs);
739        cast([
740          (arr1[0] as u128 * arr2[0] as u128 >> 64) as u64,
741          (arr1[1] as u128 * arr2[1] as u128 >> 64) as u64,
742          (arr1[2] as u128 * arr2[2] as u128 >> 64) as u64,
743          (arr1[3] as u128 * arr2[3] as u128 >> 64) as u64,
744        ])
745      } else {
746        Self {
747          a: self.a.mul_keep_high(rhs.a),
748          b: self.b.mul_keep_high(rhs.b),
749        }
750      }
751    }
752  }
753
754  optional_fn_deserialize {}
755}
756
757/// The following functionality exists only for [`u64x4`], or only for
758/// particular types inconsistently.
759impl u64x4 {
760  /// A helper for shuffle functions that turns indices of 64-bit lanes into
761  /// byte indices that can be used with 8-bit shuffle intrinsics.
762  ///
763  /// This turns each 64-bit lane `i` into eight 8-bit lanes
764  /// `[8*i, 8*i + 1, 8*i + 2, 8*i + 3, ...]`.
765  ///
766  /// This assumes `self` has already been reduced to the table's lane count,
767  /// which may be at most 32 lanes so that `8 * i` still fits in a byte.
768  #[allow(dead_code)]
769  #[inline]
770  fn to_byte_indices(self) -> u8x32 {
771    // The byte offset of the lane, broadcast to every byte of the lane.
772    let base = self.unbounded_shl_scalar(3);
773    let base = base | base.unbounded_shl_scalar(8);
774    let base = base | base.unbounded_shl_scalar(16);
775    let base = base | base.unbounded_shl_scalar(32);
776
777    // Then the offset of each byte within its lane. These bits are free because
778    // every byte of `base` is a multiple of eight. `from_ne_bytes` keeps this
779    // correct on big endian, where the bytes of a lane are the other way around.
780    const WITHIN_LANE: u64x4 =
781      u64x4::splat(u64::from_ne_bytes([0, 1, 2, 3, 4, 5, 6, 7]));
782
783    cast::<u64x4, u8x32>(base | WITHIN_LANE)
784  }
785
786  /// Returns `[self[0], b[0], self[1], b[1]]`, interleaving the low half of
787  /// each vector.
788  #[inline]
789  #[must_use]
790  pub fn unpack_lo(self, b: Self) -> Self {
791    pick! {
792      if #[cfg(target_feature="avx2")] {
793        // `unpack_low_i64_m256i` cannot be used because it acts within each
794        // 128-bit lane, which is a different operation.
795        let [aa, _]: [u64x2; 2] = cast(self);
796        let [ba, _]: [u64x2; 2] = cast(b);
797        cast([aa.unpack_lo(ba), aa.unpack_hi(ba)])
798      } else {
799        Self { a: self.a.unpack_lo(b.a), b: self.a.unpack_hi(b.a) }
800      }
801    }
802  }
803
804  /// Returns `[self[2], b[2], self[3], b[3]]`, interleaving the high half of
805  /// each vector.
806  #[inline]
807  #[must_use]
808  pub fn unpack_hi(self, b: Self) -> Self {
809    pick! {
810      if #[cfg(target_feature="avx2")] {
811        // `unpack_high_i64_m256i` cannot be used because it acts within each
812        // 128-bit lane, which is a different operation.
813        let [_, ab]: [u64x2; 2] = cast(self);
814        let [_, bb]: [u64x2; 2] = cast(b);
815        cast([ab.unpack_lo(bb), ab.unpack_hi(bb)])
816      } else {
817        Self { a: self.b.unpack_lo(b.b), b: self.b.unpack_hi(b.b) }
818      }
819    }
820  }
821
822  /// The exact per-lane product of `a` and `b` masked to `W` bits.
823  ///
824  /// Only exact for `W <= 32`, where the product still fits a lane, and
825  /// callers guard on that. It is instantiated for wider `W` too, since the
826  /// guard is a runtime `if` on a const, so it cannot assert the bound itself.
827  #[inline]
828  #[must_use]
829  pub(crate) fn mul_masked<const W: u32>(a: Self, b: Self) -> Self {
830    pick! {
831      if #[cfg(target_feature="avx2")] {
832        // `vpmuludq` reads the low 32 bits of a lane anyway, so at `W == 32` the
833        // operand masks are already implied and can be dropped. A scalar
834        // multiply has no such truncation, so the other arm always masks.
835        let (a, b) = if W == 32 {
836          (a, b)
837        } else {
838          let mask = Self::splat(add_mul_operand_mask_u64::<W>());
839          (a & mask, b & mask)
840        };
841
842        Self { avx2: mul_u64_low_bits_m256i(a.avx2, b.avx2) }
843      } else {
844        let mask = add_mul_operand_mask_u64::<W>();
845        let a = a.to_array();
846        let b = b.to_array();
847        Self::new(core::array::from_fn(|i| (a[i] & mask) * (b[i] & mask)))
848      }
849    }
850  }
851
852  /// `self + ((a * b) mod 2^W)`, reading only the low `W` bits of each lane of
853  /// `a` and `b`. `W` must be in `1..=64`.
854  #[inline]
855  #[must_use]
856  pub fn add_mul_lo<const W: u32>(self, a: Self, b: Self) -> Self {
857    pick! {
858      if #[cfg(all(target_feature="avx512ifma", target_feature="avx512vl"))] {
859        // IFMA is fixed at 52 bits; any other width takes the generic path.
860        if W == 52 {
861          return Self {
862            avx2: add_mul_low_u52_m256i(self.avx2, a.avx2, b.avx2),
863          };
864        }
865      }
866    }
867
868    // Below 33 bits the whole product fits a lane, so one widening multiply
869    // yields both halves and the split is a mask rather than an instruction.
870    if W <= 32 {
871      let mask = Self::splat(add_mul_operand_mask_u64::<W>());
872      return self + (Self::mul_masked::<W>(a, b) & mask);
873    }
874
875    let acc = self.to_array();
876    let a = a.to_array();
877    let b = b.to_array();
878    Self::new(core::array::from_fn(|i| {
879      add_mul_lo_lane_u64::<W>(acc[i], a[i], b[i])
880    }))
881  }
882
883  /// `self + ((a * b) >> W)`, reading only the low `W` bits of each lane of `a`
884  /// and `b`. `W` must be in `1..=64`.
885  #[inline]
886  #[must_use]
887  pub fn add_mul_hi<const W: u32>(self, a: Self, b: Self) -> Self {
888    pick! {
889      if #[cfg(all(target_feature="avx512ifma", target_feature="avx512vl"))] {
890        // IFMA is fixed at 52 bits; any other width takes the generic path.
891        if W == 52 {
892          return Self {
893            avx2: add_mul_high_u52_m256i(self.avx2, a.avx2, b.avx2),
894          };
895        }
896      }
897    }
898
899    // See `add_mul_lo`: the whole product is in the lane, so the high half is a
900    // shift.
901    if W <= 32 {
902      return self + (Self::mul_masked::<W>(a, b) >> W);
903    }
904
905    let acc = self.to_array();
906    let a = a.to_array();
907    let b = b.to_array();
908    Self::new(core::array::from_fn(|i| {
909      add_mul_hi_lane_u64::<W>(acc[i], a[i], b[i])
910    }))
911  }
912}