Skip to main content

simd_popcnt/
lib.rs

1//! # simd-popcnt
2//!
3//! Count the number of 1 bits (bit population count, a.k.a. Hamming weight) in
4//! an array as quickly as possible using specialized CPU instructions: POPCNT,
5//! AVX2 and AVX512 on x86/x86-64, and NEON and SVE on AArch64. The fastest
6//! instruction set the CPU supports is detected once at runtime and cached; on
7//! every other architecture the count falls back to [`u64::count_ones`], which
8//! the compiler lowers to a hardware popcount instruction wherever one exists.
9//!
10//! The crate is portable by default and thread-safe. It has no external crate
11//! dependencies and needs the Rust standard library only for runtime SIMD
12//! dispatch (CPU feature detection); it is otherwise `no_std`.
13//!
14//! This is an AI-assisted Rust port of the [libpopcnt C/C++ library](https://github.com/kimwalisch/libpopcnt).
15//!
16//! ## Usage
17//!
18//! [`popcnt`] counts the 1 bits in a byte slice; the [`PopcntExt`] trait adds a
19//! `.popcnt()` method to slices, arrays and `Vec`s of every built-in integer
20//! type.
21//!
22//! ```
23//! use simd_popcnt::{popcnt, PopcntExt};
24//!
25//! assert_eq!(popcnt(&[0xFF, 0x0F]), 12);
26//! assert_eq!([u64::MAX, 0x0F0F_0F0F_0F0F_0F0F].popcnt(), 96);
27//! ```
28//!
29//! ## Performance
30//!
31//! For the fastest possible code, compile with `RUSTFLAGS="-C target-cpu=native"`.
32//! This selects the best SIMD path at compile time and removes the runtime
33//! dispatch entirely.
34
35// Enable the SVE intrinsics only when the build probe confirmed they compile and
36// the SVE code is actually built (compile-time SVE path or the `std` dispatcher).
37#![cfg_attr(
38    all(simd_popcnt_have_sve, any(target_feature = "sve", feature = "std")),
39    feature(stdarch_aarch64_sve)
40)]
41// `std` is used only for runtime CPU feature detection. When that's absent —
42// `std` feature off, `-C target-cpu=native`, or a non-x86/AArch64 target — the
43// crate is `no_std`. `not(test)` keeps `std` for the unit tests.
44#![cfg_attr(
45    not(any(
46        test,
47        all(
48            feature = "std",
49            any(target_arch = "x86", target_arch = "x86_64"),
50            not(any(target_feature = "avx2", target_feature = "avx512vpopcntdq")),
51        ),
52        all(
53            feature = "std",
54            target_arch = "aarch64",
55            simd_popcnt_have_sve,
56            not(target_feature = "sve"),
57        ),
58    )),
59    no_std
60)]
61
62#[cfg(target_arch = "aarch64")]
63use core::arch::aarch64::*;
64// A scalar-only `no_std` build uses none of these x86 intrinsics.
65#[cfg(target_arch = "x86")]
66#[allow(unused_imports)]
67use core::arch::x86::*;
68#[cfg(target_arch = "x86_64")]
69#[allow(unused_imports)]
70use core::arch::x86_64::*;
71#[cfg(all(
72    target_arch = "aarch64",
73    simd_popcnt_have_sve,
74    feature = "std",
75    not(target_feature = "sve")
76))]
77use std::arch::is_aarch64_feature_detected;
78
79/// Counts the number of one bits (population count) in `bytes`.
80///
81/// Dispatches to the fastest implementation for the running CPU: SIMD where
82/// available, a scalar fallback otherwise.
83///
84/// To count the bits in a slice of a wider integer type (`&[u64]`, `&[u32]`, …),
85/// use the [`PopcntExt::popcnt`] method rather than converting to bytes by hand.
86///
87/// # Examples
88///
89/// ```
90/// assert_eq!(simd_popcnt::popcnt(&[]), 0);
91/// assert_eq!(simd_popcnt::popcnt(&[0xFF, 0x0F]), 12);
92/// ```
93#[must_use]
94#[inline]
95pub fn popcnt(bytes: &[u8]) -> u64 {
96    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
97    {
98        popcnt_x86(bytes)
99    }
100
101    #[cfg(target_arch = "aarch64")]
102    {
103        popcnt_aarch64(bytes)
104    }
105
106    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))]
107    {
108        popcnt_scalar(bytes)
109    }
110}
111
112// ────────────────────────────────────────────────────────────────────────────
113// Extension trait for integer slices
114// ────────────────────────────────────────────────────────────────────────────
115
116/// Adds a [`popcnt`](PopcntExt::popcnt) method to slices of the built-in integer
117/// types, counting their bits without a manual byte cast. Implemented for slices,
118/// arrays and `Vec`s of `u8`/`u16`/`u32`/`u64`/`u128`/`usize` and their signed
119/// counterparts; bring it into scope with `use simd_popcnt::PopcntExt;`.
120///
121/// ```
122/// use simd_popcnt::PopcntExt;
123///
124/// let words: &[u64] = &[u64::MAX, 0x0F0F_0F0F_0F0F_0F0F];
125/// assert_eq!(words.popcnt(), 64 + 32);
126/// assert_eq!(vec![1u32, 2, 3].popcnt(), 4);
127/// ```
128pub trait PopcntExt {
129    /// Count the total number of 1 bits across all elements of the slice.
130    #[must_use]
131    fn popcnt(&self) -> u64;
132}
133
134/// Implements [`PopcntExt`] for `[$t]` by reinterpreting the slice as bytes.
135/// Correct on either endianness since popcount is byte-order independent.
136macro_rules! impl_popcnt_ext {
137    ($($t:ty),+ $(,)?) => {$(
138        impl PopcntExt for [$t] {
139            #[inline]
140            fn popcnt(&self) -> u64 {
141                // SAFETY: `$t` is a plain integer (no padding, every bit pattern
142                // valid) and `u8` is always 1-aligned, so the slice is a valid
143                // `&[u8]` of `size_of_val` bytes.
144                let bytes = unsafe {
145                    core::slice::from_raw_parts(
146                        self.as_ptr().cast::<u8>(),
147                        core::mem::size_of_val(self),
148                    )
149                };
150                popcnt(bytes)
151            }
152        }
153    )+};
154}
155
156impl_popcnt_ext!(
157    u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize
158);
159
160// ────────────────────────────────────────────────────────────────────────────
161// Portable scalar fallbacks
162// ────────────────────────────────────────────────────────────────────────────
163
164/// Packs the 0..=7 trailing bytes into a `u64` (native byte order; popcount is
165/// order-independent). Shift-or rather than `copy_from_slice`, which lowers to a
166/// `memcpy` call for the runtime length.
167#[inline]
168fn tail_u64(rem: &[u8]) -> u64 {
169    let mut v = 0u64;
170    for (j, &b) in rem.iter().enumerate() {
171        v |= (b as u64) << (j * 8);
172    }
173    v
174}
175
176/// Scalar popcount over 8-byte chunks. `count_ones()` lowers to a hardware
177/// popcount where the target has one, else to inline bit-twiddling — never a
178/// libcall.
179macro_rules! popcnt_scalar_loop {
180    ($bytes:expr) => {{
181        let mut cnt = 0u64;
182        let (chunks, rem) = $bytes.as_chunks::<8>();
183        for chunk in chunks {
184            cnt += u64::from_ne_bytes(*chunk).count_ones() as u64;
185        }
186        if !rem.is_empty() {
187            cnt += tail_u64(rem).count_ones() as u64;
188        }
189        cnt
190    }};
191}
192
193/// Portable scalar population count via [`u64::count_ones`].
194#[allow(dead_code)]
195#[inline]
196fn popcnt_scalar(bytes: &[u8]) -> u64 {
197    popcnt_scalar_loop!(bytes)
198}
199
200// ════════════════════════════════════════════════════════════════════════════
201// x86 / x86-64
202// ════════════════════════════════════════════════════════════════════════════
203
204#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
205#[inline]
206fn popcnt_x86(bytes: &[u8]) -> u64 {
207    // Compile-time AVX512 path (e.g. with `-C target-cpu=native`).
208    #[cfg(target_feature = "avx512vpopcntdq")]
209    {
210        // AVX512 isn't worth its setup cost for tiny arrays.
211        if bytes.len() >= 40 {
212            unsafe { popcnt_avx512(bytes) }
213        } else {
214            popcnt_scalar_static(bytes)
215        }
216    }
217
218    // Compile-time AVX2 path.
219    #[cfg(all(target_feature = "avx2", not(target_feature = "avx512vpopcntdq")))]
220    {
221        let mut cnt = 0u64;
222        let mut rest = bytes;
223        // Scalar below ~96 bytes, a `popcnt256` loop for the medium range,
224        // Harley-Seal from ~1 KB.
225        if bytes.len() >= 96 {
226            let n = bytes.len() / 32 * 32;
227            cnt += if bytes.len() >= 1024 {
228                unsafe { popcnt_avx2(&bytes[..n]) }
229            } else {
230                unsafe { popcnt_avx2_medium(&bytes[..n]) }
231            };
232            rest = &bytes[n..];
233        }
234        cnt + popcnt_scalar_static(rest)
235    }
236
237    // No SIMD enabled at compile time: detect at runtime (needs `std`).
238    #[cfg(all(
239        not(any(target_feature = "avx2", target_feature = "avx512vpopcntdq")),
240        feature = "std"
241    ))]
242    {
243        popcnt_x86_runtime(bytes)
244    }
245
246    // No SIMD and no `std` for runtime detection: use the compile-time scalar path.
247    #[cfg(all(
248        not(any(target_feature = "avx2", target_feature = "avx512vpopcntdq")),
249        not(feature = "std")
250    ))]
251    {
252        popcnt_scalar_static(bytes)
253    }
254}
255
256/// Scalar count for the compile-time SIMD paths' small arrays and tails:
257/// hardware POPCNT when statically enabled, otherwise the integer fallback.
258#[cfg(all(
259    any(target_arch = "x86", target_arch = "x86_64"),
260    any(
261        target_feature = "avx2",
262        target_feature = "avx512vpopcntdq",
263        not(feature = "std")
264    )
265))]
266#[inline]
267fn popcnt_scalar_static(bytes: &[u8]) -> u64 {
268    #[cfg(target_feature = "popcnt")]
269    {
270        // SAFETY: `popcnt` is statically enabled for the whole crate.
271        unsafe { popcnt_scalar_hw(bytes) }
272    }
273    #[cfg(not(target_feature = "popcnt"))]
274    {
275        popcnt_scalar(bytes)
276    }
277}
278
279/// Cached check for AVX-512F + BW + VPOPCNTDQ, so repeat calls load one atomic
280/// instead of re-running three `is_x86_feature_detected!` probes.
281#[cfg(all(
282    any(target_arch = "x86", target_arch = "x86_64"),
283    not(any(target_feature = "avx2", target_feature = "avx512vpopcntdq")),
284    feature = "std"
285))]
286#[inline]
287fn has_avx512() -> bool {
288    use core::sync::atomic::{AtomicI32, Ordering};
289    static HAS_AVX512: AtomicI32 = AtomicI32::new(-1);
290    let cached = HAS_AVX512.load(Ordering::Relaxed);
291    if cached != -1 {
292        return cached != 0;
293    }
294    let v = (is_x86_feature_detected!("avx512f")
295        && is_x86_feature_detected!("avx512bw")
296        && is_x86_feature_detected!("avx512vpopcntdq")) as i32;
297    HAS_AVX512.store(v, Ordering::Relaxed);
298    v != 0
299}
300
301/// Runtime dispatch using cached CPU feature detection. Only compiled when no
302/// SIMD feature is statically enabled (otherwise the compile-time paths run).
303#[cfg(all(
304    any(target_arch = "x86", target_arch = "x86_64"),
305    not(any(target_feature = "avx2", target_feature = "avx512vpopcntdq")),
306    feature = "std"
307))]
308#[inline]
309fn popcnt_x86_runtime(bytes: &[u8]) -> u64 {
310    // AVX512: not worth its setup cost below ~40 bytes, handles any length.
311    if bytes.len() >= 40 && has_avx512() {
312        return unsafe { popcnt_avx512(bytes) };
313    }
314
315    let mut cnt = 0u64;
316    let mut rest = bytes;
317
318    // AVX2: a plain `popcnt256` loop for the medium range, Harley-Seal from
319    // ~1 KB up. Below ~96 bytes scalar POPCNT is faster (on pre-Ice-Lake CPUs
320    // its false-dependency-bound loop still beats AVX2 there); the `popcnt256`
321    // loop beats Harley-Seal until ~1 KB. Thresholds follow the sse-popcount
322    // benchmarks across Haswell..Cascadelake.
323    if bytes.len() >= 96 && is_x86_feature_detected!("avx2") {
324        let n = bytes.len() / 32 * 32;
325        cnt += if bytes.len() >= 1024 {
326            unsafe { popcnt_avx2(&bytes[..n]) }
327        } else {
328            unsafe { popcnt_avx2_medium(&bytes[..n]) }
329        };
330        rest = &bytes[n..];
331    }
332
333    // Scalar tail, or the whole array if AVX2 didn't fire. The POPCNT dispatch
334    // matters: outside a `target_feature` fn, `count_ones()` stays a software
335    // fallback even on POPCNT CPUs.
336    cnt += if is_x86_feature_detected!("popcnt") {
337        // SAFETY: POPCNT confirmed above. x86-64 uses the inline-asm loop (it
338        // inlines here, unlike the `target_feature` fn); x86 has no 64-bit popcnt
339        // register, so it keeps `popcnt_scalar_hw`.
340        #[cfg(target_arch = "x86_64")]
341        {
342            unsafe { popcnt_scalar_asm(rest) }
343        }
344        #[cfg(target_arch = "x86")]
345        {
346            unsafe { popcnt_scalar_hw(rest) }
347        }
348    } else {
349        popcnt_scalar(rest)
350    };
351
352    cnt
353}
354
355/// Scalar population count via the hardware POPCNT instruction. The
356/// `#[target_feature(enable = "popcnt")]` attribute is what lets `count_ones()`
357/// lower to a single `popcnt`; only call it once POPCNT support is confirmed.
358#[allow(dead_code)]
359#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
360#[target_feature(enable = "popcnt")]
361#[inline]
362fn popcnt_scalar_hw(bytes: &[u8]) -> u64 {
363    popcnt_scalar_loop!(bytes) // count_ones() lowers to popcntq here
364}
365
366/// `u64` popcount via inline-asm `popcnt`. `count_ones()` and the `_popcnt64`
367/// intrinsic only emit the instruction inside a `#[target_feature(enable =
368/// "popcnt")]` fn, which then can't be inlined into the feature-less dispatcher;
369/// inline asm has no such barrier and folds into the caller (as libpopcnt's
370/// `__asm__("popcnt")` and MSVC's `__popcnt64` do).
371///
372/// The instruction is emitted unconditionally, so callers must confirm POPCNT at
373/// runtime first — hence `unsafe`.
374#[allow(dead_code)]
375#[cfg(target_arch = "x86_64")]
376#[inline(always)]
377unsafe fn popcnt64_asm(x: u64) -> u64 {
378    let out: u64;
379    // Pure reg→reg, no memory (`pure`/`nomem`); `popcnt` writes ZF, so not `preserves_flags`.
380    unsafe {
381        core::arch::asm!(
382            "popcnt {out}, {inp}",
383            inp = in(reg) x,
384            out = out(reg) out,
385            options(pure, nomem, nostack),
386        );
387    }
388    out
389}
390
391/// Scalar loop over [`popcnt64_asm`]; no `target_feature` attribute, so it
392/// inlines into the dispatcher. Sound only after a runtime POPCNT check.
393#[allow(dead_code)]
394#[cfg(target_arch = "x86_64")]
395#[inline(always)]
396unsafe fn popcnt_scalar_asm(bytes: &[u8]) -> u64 {
397    let mut cnt = 0u64;
398    let (chunks, rem) = bytes.as_chunks::<8>();
399    for chunk in chunks {
400        // SAFETY: POPCNT confirmed by the caller.
401        cnt += unsafe { popcnt64_asm(u64::from_ne_bytes(*chunk)) };
402    }
403    if !rem.is_empty() {
404        cnt += unsafe { popcnt64_asm(tail_u64(rem)) };
405    }
406    cnt
407}
408
409// ── AVX2 ────────────────────────────────────────────────────────────────────
410
411/// Carry-save adder: returns the `(carry, sum)` bit-planes of `a + b + c`,
412/// computed across all lanes in parallel.
413#[cfg(all(
414    any(target_arch = "x86", target_arch = "x86_64"),
415    not(target_feature = "avx512vpopcntdq"),
416    any(target_feature = "avx2", feature = "std")
417))]
418#[target_feature(enable = "avx2")]
419#[inline]
420fn csa256(a: __m256i, b: __m256i, c: __m256i) -> (__m256i, __m256i) {
421    let u = _mm256_xor_si256(a, b);
422    let h = _mm256_or_si256(_mm256_and_si256(a, b), _mm256_and_si256(u, c));
423    let l = _mm256_xor_si256(u, c);
424    (h, l)
425}
426
427/// Per-byte population count of a 256-bit vector using the nibble lookup, then
428/// horizontal sum of each 8-byte lane via `_mm256_sad_epu8` (result in 4 u64s).
429#[cfg(all(
430    any(target_arch = "x86", target_arch = "x86_64"),
431    not(target_feature = "avx512vpopcntdq"),
432    any(target_feature = "avx2", feature = "std")
433))]
434#[target_feature(enable = "avx2")]
435#[inline]
436fn popcnt256(v: __m256i) -> __m256i {
437    let lookup1 = _mm256_setr_epi8(
438        4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7,
439        7, 8,
440    );
441    let lookup2 = _mm256_setr_epi8(
442        4, 3, 3, 2, 3, 2, 2, 1, 3, 2, 2, 1, 2, 1, 1, 0, 4, 3, 3, 2, 3, 2, 2, 1, 3, 2, 2, 1, 2, 1,
443        1, 0,
444    );
445    let low_mask = _mm256_set1_epi8(0x0f);
446    let lo = _mm256_and_si256(v, low_mask);
447    let hi = _mm256_and_si256(_mm256_srli_epi16(v, 4), low_mask);
448    let popcnt1 = _mm256_shuffle_epi8(lookup1, lo);
449    let popcnt2 = _mm256_shuffle_epi8(lookup2, hi);
450    _mm256_sad_epu8(popcnt1, popcnt2)
451}
452
453/// AVX2 Harley-Seal population count (4th iteration), from "Faster Population
454/// Counts using AVX2 Instructions" by Lemire, Kurz and Muła (2016),
455/// <https://arxiv.org/abs/1611.07612>.
456///
457/// `bytes.len()` must be a multiple of 32.
458#[cfg(all(
459    any(target_arch = "x86", target_arch = "x86_64"),
460    not(target_feature = "avx512vpopcntdq"),
461    any(target_feature = "avx2", feature = "std")
462))]
463#[target_feature(enable = "avx2")]
464#[inline]
465// Hand-aligned: keep the 16-way CSA tree readable.
466#[rustfmt::skip]
467fn popcnt_avx2(bytes: &[u8]) -> u64 {
468    let zero = _mm256_setzero_si256();
469    let mut cnt = zero;
470    let mut ones = zero;
471    let mut twos = zero;
472    let mut fours = zero;
473    let mut eights = zero;
474    let mut twos_a;
475    let mut twos_b;
476    let mut fours_a;
477    let mut fours_b;
478    let mut eights_a;
479    let mut eights_b;
480    let mut sixteens;
481
482    // 16 vectors (512 bytes) per iteration.
483    let (blocks, tail) = bytes.as_chunks::<512>();
484    for chunk in blocks {
485        let p = chunk.as_ptr().cast::<__m256i>();
486        // SAFETY: `chunk` is 512 bytes, so all 16 loads (32 bytes each) are in bounds.
487        unsafe {
488            (twos_a, ones) = csa256(ones, _mm256_loadu_si256(p.add(0)), _mm256_loadu_si256(p.add(1)));
489            (twos_b, ones) = csa256(ones, _mm256_loadu_si256(p.add(2)), _mm256_loadu_si256(p.add(3)));
490            (fours_a, twos) = csa256(twos, twos_a, twos_b);
491            (twos_a, ones) = csa256(ones, _mm256_loadu_si256(p.add(4)), _mm256_loadu_si256(p.add(5)));
492            (twos_b, ones) = csa256(ones, _mm256_loadu_si256(p.add(6)), _mm256_loadu_si256(p.add(7)));
493            (fours_b, twos) = csa256(twos, twos_a, twos_b);
494            (eights_a, fours) = csa256(fours, fours_a, fours_b);
495            (twos_a, ones) = csa256(ones, _mm256_loadu_si256(p.add(8)), _mm256_loadu_si256(p.add(9)));
496            (twos_b, ones) = csa256(ones, _mm256_loadu_si256(p.add(10)), _mm256_loadu_si256(p.add(11)));
497            (fours_a, twos) = csa256(twos, twos_a, twos_b);
498            (twos_a, ones) = csa256(ones, _mm256_loadu_si256(p.add(12)), _mm256_loadu_si256(p.add(13)));
499            (twos_b, ones) = csa256(ones, _mm256_loadu_si256(p.add(14)), _mm256_loadu_si256(p.add(15)));
500            (fours_b, twos) = csa256(twos, twos_a, twos_b);
501            (eights_b, fours) = csa256(fours, fours_a, fours_b);
502            (sixteens, eights) = csa256(eights, eights_a, eights_b);
503            cnt = _mm256_add_epi64(cnt, popcnt256(sixteens));
504        }
505    }
506
507    cnt = _mm256_slli_epi64(cnt, 4);
508    cnt = _mm256_add_epi64(cnt, _mm256_slli_epi64(popcnt256(eights), 3));
509    cnt = _mm256_add_epi64(cnt, _mm256_slli_epi64(popcnt256(fours), 2));
510    cnt = _mm256_add_epi64(cnt, _mm256_slli_epi64(popcnt256(twos), 1));
511    cnt = _mm256_add_epi64(cnt, popcnt256(ones));
512
513    // Remaining whole 32-byte vectors.
514    let (vecs, _) = tail.as_chunks::<32>();
515    for chunk in vecs {
516        let v = unsafe { _mm256_loadu_si256(chunk.as_ptr().cast::<__m256i>()) };
517        cnt = _mm256_add_epi64(cnt, popcnt256(v));
518    }
519
520    // Sum the four 64-bit lanes.
521    // SAFETY: `__m256i` and `[u64; 4]` are both 32 bytes with no invalid bit patterns.
522    let lanes: [u64; 4] = unsafe { core::mem::transmute(cnt) };
523    lanes[0] + lanes[1] + lanes[2] + lanes[3]
524}
525
526/// Plain single-accumulator `popcnt256` loop for medium arrays (~96 bytes to
527/// ~1 KB). No unrolling or extra accumulators: these arrays are only a handful
528/// of vectors, so the accumulator dependency chain never bottlenecks and the
529/// simpler loop is a touch faster. It beats scalar from ~96 bytes, and beats
530/// Harley-Seal — whose fixed CSA-reduction epilogue dominates at these sizes —
531/// until ~1 KB. `bytes.len()` must be a multiple of 32.
532#[cfg(all(
533    any(target_arch = "x86", target_arch = "x86_64"),
534    not(target_feature = "avx512vpopcntdq"),
535    any(target_feature = "avx2", feature = "std")
536))]
537#[target_feature(enable = "avx2")]
538#[inline]
539fn popcnt_avx2_medium(bytes: &[u8]) -> u64 {
540    let mut acc = _mm256_setzero_si256();
541    let (vecs, _) = bytes.as_chunks::<32>();
542    for chunk in vecs {
543        let v = unsafe { _mm256_loadu_si256(chunk.as_ptr().cast::<__m256i>()) };
544        acc = _mm256_add_epi64(acc, popcnt256(v));
545    }
546    // SAFETY: `__m256i` and `[u64; 4]` are both 32 bytes with no invalid bit patterns.
547    let lanes: [u64; 4] = unsafe { core::mem::transmute(acc) };
548    lanes[0] + lanes[1] + lanes[2] + lanes[3]
549}
550
551// ── AVX512 ──────────────────────────────────────────────────────────────────
552
553/// AVX512-VPOPCNTDQ population count, handling any length: a 4×-unrolled
554/// 256-byte loop, then a 64-byte loop, then a masked load for the final
555/// 1..=63 bytes.
556#[cfg(all(
557    any(target_arch = "x86", target_arch = "x86_64"),
558    any(
559        all(not(target_feature = "avx2"), feature = "std"),
560        target_feature = "avx512vpopcntdq"
561    )
562))]
563#[target_feature(enable = "avx512f,avx512bw,avx512vpopcntdq")]
564#[inline]
565fn popcnt_avx512(bytes: &[u8]) -> u64 {
566    let mut cnt0 = _mm512_setzero_si512();
567
568    // 4× unrolled 64-byte loop (256 bytes per iteration). Four independent
569    // accumulators keep the popcount+add chains parallel (higher ILP).
570    let (blocks, tail256) = bytes.as_chunks::<256>();
571    if !blocks.is_empty() {
572        let mut cnt1 = _mm512_setzero_si512();
573        let mut cnt2 = _mm512_setzero_si512();
574        let mut cnt3 = _mm512_setzero_si512();
575        for chunk in blocks {
576            let p = chunk.as_ptr();
577            // SAFETY: `chunk` is 256 bytes, so the four 64-byte loads are in bounds.
578            unsafe {
579                let v0 = _mm512_loadu_si512(p.add(0).cast());
580                let v1 = _mm512_loadu_si512(p.add(64).cast());
581                let v2 = _mm512_loadu_si512(p.add(128).cast());
582                let v3 = _mm512_loadu_si512(p.add(192).cast());
583                cnt0 = _mm512_add_epi64(cnt0, _mm512_popcnt_epi64(v0));
584                cnt1 = _mm512_add_epi64(cnt1, _mm512_popcnt_epi64(v1));
585                cnt2 = _mm512_add_epi64(cnt2, _mm512_popcnt_epi64(v2));
586                cnt3 = _mm512_add_epi64(cnt3, _mm512_popcnt_epi64(v3));
587            }
588        }
589        cnt0 = _mm512_add_epi64(cnt0, cnt1);
590        cnt2 = _mm512_add_epi64(cnt2, cnt3);
591        cnt0 = _mm512_add_epi64(cnt0, cnt2);
592    }
593
594    // Remaining complete 64-byte blocks.
595    let (vecs, tail64) = tail256.as_chunks::<64>();
596    for chunk in vecs {
597        let v = unsafe { _mm512_loadu_si512(chunk.as_ptr().cast()) };
598        cnt0 = _mm512_add_epi64(cnt0, _mm512_popcnt_epi64(v));
599    }
600
601    // Masked load for the final 1..=63 bytes.
602    if !tail64.is_empty() {
603        let len = tail64.len();
604        let mask = (u64::MAX >> (64 - len)) as __mmask64;
605        // SAFETY: the mask selects only the `len` valid bytes; masked-off lanes
606        // are not accessed.
607        unsafe {
608            let v = _mm512_maskz_loadu_epi8(mask, tail64.as_ptr().cast());
609            cnt0 = _mm512_add_epi64(cnt0, _mm512_popcnt_epi64(v));
610        }
611    }
612
613    _mm512_reduce_add_epi64(cnt0) as u64
614}
615
616// ════════════════════════════════════════════════════════════════════════════
617// AArch64
618// ════════════════════════════════════════════════════════════════════════════
619
620#[cfg(target_arch = "aarch64")]
621#[inline]
622fn popcnt_aarch64(bytes: &[u8]) -> u64 {
623    // Compile-time SVE path.
624    #[cfg(all(target_feature = "sve", simd_popcnt_have_sve))]
625    {
626        unsafe { popcnt_arm_sve(bytes) }
627    }
628
629    // NEON baseline; `popcnt_neon` dispatches to SVE at runtime when available.
630    #[cfg(not(all(target_feature = "sve", simd_popcnt_have_sve)))]
631    {
632        popcnt_neon(bytes)
633    }
634}
635
636#[cfg(all(
637    target_arch = "aarch64",
638    not(all(target_feature = "sve", simd_popcnt_have_sve))
639))]
640#[inline]
641fn vpadalq(sum: uint64x2_t, t: uint8x16_t) -> uint64x2_t {
642    unsafe { vpadalq_u32(sum, vpaddlq_u16(vpaddlq_u8(t))) }
643}
644
645#[cfg(all(
646    target_arch = "aarch64",
647    not(all(target_feature = "sve", simd_popcnt_have_sve))
648))]
649#[inline]
650fn popcnt_neon(bytes: &[u8]) -> u64 {
651    #[cfg(all(simd_popcnt_have_sve, feature = "std"))]
652    if is_aarch64_feature_detected!("sve") {
653        return unsafe { popcnt_arm_sve(bytes) };
654    }
655
656    const CHUNK: usize = 64;
657    let mut cnt = 0u64;
658    let iters = bytes.len() / CHUNK;
659    let ptr = bytes.as_ptr();
660
661    if iters > 0 {
662        // SAFETY: `iters = len / 64`, so every load at `i * 64` (i < iters) reads
663        // 64 in-bounds bytes; the final store targets a local array.
664        unsafe {
665            let mut sum = vdupq_n_u64(0);
666            let zero = vdupq_n_u8(0);
667            let mut i = 0usize;
668
669            while i < iters {
670                let mut t0 = zero;
671                let mut t1 = zero;
672                let mut t2 = zero;
673                let mut t3 = zero;
674
675                // Accumulate at most 31 chunks before draining into `sum`:
676                // 31 × 8 bits = 248 ≤ 255 guarantees no u8 lane overflow.
677                let limit = (i + 31).min(iters);
678                while i < limit {
679                    // Plain contiguous load (`vld1q_u8_x4`), not the deinterleaving
680                    // `vld4q_u8`: population count is order-independent, so avoiding
681                    // the deinterleave saves the `tbl`/`mov` shuffles it compiles to.
682                    let input = vld1q_u8_x4(ptr.add(i * CHUNK));
683                    t0 = vaddq_u8(t0, vcntq_u8(input.0));
684                    t1 = vaddq_u8(t1, vcntq_u8(input.1));
685                    t2 = vaddq_u8(t2, vcntq_u8(input.2));
686                    t3 = vaddq_u8(t3, vcntq_u8(input.3));
687                    i += 1;
688                }
689
690                sum = vpadalq(sum, t0);
691                sum = vpadalq(sum, t1);
692                sum = vpadalq(sum, t2);
693                sum = vpadalq(sum, t3);
694            }
695
696            let mut tmp = [0u64; 2];
697            vst1q_u64(tmp.as_mut_ptr(), sum);
698            cnt += tmp[0] + tmp[1];
699        }
700    }
701
702    // Scalar tail. On AArch64 `count_ones()` always lowers to NEON `cnt`, so no
703    // POPCNT runtime check is needed here.
704    let rest = &bytes[iters * CHUNK..];
705    cnt += popcnt_scalar_loop!(rest);
706    cnt
707}
708
709// ── ARM SVE ─────────────────────────────────────────────────────────────────
710
711/// SVE population count: a 4×-unrolled main loop over full vectors, then a
712/// predicated tail loop that needs no separate scalar remainder.
713#[cfg(all(
714    target_arch = "aarch64",
715    simd_popcnt_have_sve,
716    any(target_feature = "sve", feature = "std")
717))]
718#[target_feature(enable = "sve")]
719#[inline]
720fn popcnt_arm_sve(bytes: &[u8]) -> u64 {
721    // SAFETY: the loop bound keeps each full load within `len`; the tail loop's
722    // predicate masks off any lanes past the end.
723    unsafe {
724        let mut i = 0usize;
725        let mut vcnt0 = svdup_n_u64(0);
726        let vl = svcntb() as usize; // SVE vector length in bytes (hardware-defined)
727        let ptr = bytes.as_ptr();
728        let len = bytes.len();
729
730        // 4× unrolled full-predicate loop. Four independent accumulators keep the
731        // count+add chains parallel (higher ILP).
732        if i + vl * 4 <= len {
733            let mut vcnt1 = svdup_n_u64(0);
734            let mut vcnt2 = svdup_n_u64(0);
735            let mut vcnt3 = svdup_n_u64(0);
736            loop {
737                let v0 = svreinterpret_u64_u8(svld1_u8(svptrue_b8(), ptr.add(i)));
738                let v1 = svreinterpret_u64_u8(svld1_u8(svptrue_b8(), ptr.add(i + vl)));
739                let v2 = svreinterpret_u64_u8(svld1_u8(svptrue_b8(), ptr.add(i + vl * 2)));
740                let v3 = svreinterpret_u64_u8(svld1_u8(svptrue_b8(), ptr.add(i + vl * 3)));
741                vcnt0 = svadd_u64_x(svptrue_b64(), vcnt0, svcnt_u64_x(svptrue_b64(), v0));
742                vcnt1 = svadd_u64_x(svptrue_b64(), vcnt1, svcnt_u64_x(svptrue_b64(), v1));
743                vcnt2 = svadd_u64_x(svptrue_b64(), vcnt2, svcnt_u64_x(svptrue_b64(), v2));
744                vcnt3 = svadd_u64_x(svptrue_b64(), vcnt3, svcnt_u64_x(svptrue_b64(), v3));
745                i += vl * 4;
746                if i + vl * 4 > len {
747                    break;
748                }
749            }
750            vcnt0 = svadd_u64_x(svptrue_b64(), vcnt0, vcnt1);
751            vcnt2 = svadd_u64_x(svptrue_b64(), vcnt2, vcnt3);
752            vcnt0 = svadd_u64_x(svptrue_b64(), vcnt0, vcnt2);
753        }
754
755        // Predicated tail: the load zero-fills inactive lanes, so no separate
756        // scalar remainder is needed.
757        let mut pg = svwhilelt_b8_u64(i as u64, len as u64);
758        while svptest_any(svptrue_b8(), pg) {
759            let v = svreinterpret_u64_u8(svld1_u8(pg, ptr.add(i)));
760            vcnt0 = svadd_u64_x(svptrue_b64(), vcnt0, svcnt_u64_x(svptrue_b64(), v));
761            i += vl;
762            pg = svwhilelt_b8_u64(i as u64, len as u64);
763        }
764
765        svaddv_u64(svptrue_b64(), vcnt0)
766    }
767}
768
769// ════════════════════════════════════════════════════════════════════════════
770// Tests
771// ════════════════════════════════════════════════════════════════════════════
772
773#[cfg(test)]
774mod tests {
775    use super::*;
776
777    /// Reference implementation: count bits one byte at a time.
778    fn reference(bytes: &[u8]) -> u64 {
779        bytes.iter().map(|b| b.count_ones() as u64).sum()
780    }
781
782    /// Independent integer-only popcount oracle (does not use `count_ones`),
783    /// so the sweep cross-checks the crate against a different algorithm.
784    fn popcnt64_bitwise(x: u64) -> u64 {
785        const M1: u64 = 0x5555555555555555;
786        const M2: u64 = 0x3333333333333333;
787        const M4: u64 = 0x0F0F0F0F0F0F0F0F;
788        const H01: u64 = 0x0101010101010101;
789        let x = x - ((x >> 1) & M1);
790        let x = (x & M2) + ((x >> 2) & M2);
791        let x = (x + (x >> 4)) & M4;
792        x.wrapping_mul(H01) >> 56
793    }
794
795    #[test]
796    fn empty() {
797        assert_eq!(popcnt(&[]), 0);
798    }
799
800    #[test]
801    fn all_ones() {
802        for &size in &[
803            0, 1, 7, 8, 31, 32, 39, 40, 63, 64, 255, 256, 511, 512, 4095, 4096, 65537,
804        ] {
805            let bytes = vec![0xFFu8; size];
806            assert_eq!(popcnt(&bytes), size as u64 * 8, "size={size}");
807        }
808    }
809
810    #[test]
811    fn all_zeros() {
812        let bytes = vec![0u8; 65536];
813        assert_eq!(popcnt(&bytes), 0);
814    }
815
816    #[test]
817    fn single_bits() {
818        for bit in 0u64..64 {
819            let val = 1u64 << bit;
820            assert_eq!(popcnt(&val.to_le_bytes()), 1, "bit={bit}");
821        }
822    }
823
824    /// `PopcntExt::popcnt` on each integer width must equal the per-element
825    /// `count_ones()` sum (an oracle independent of the byte reinterpretation).
826    #[test]
827    fn ext_trait_widths() {
828        let u8s: &[u8] = &[0xFF, 0x0F, 0x00, 0xAB, 0x01];
829        assert_eq!(
830            u8s.popcnt(),
831            u8s.iter().map(|x| x.count_ones() as u64).sum()
832        );
833
834        let u16s: &[u16] = &[0xFFFF, 0x0F0F, 0x1234, 0];
835        assert_eq!(
836            u16s.popcnt(),
837            u16s.iter().map(|x| x.count_ones() as u64).sum()
838        );
839
840        let u32s: &[u32] = &[u32::MAX, 0, 0x8000_0001];
841        assert_eq!(
842            u32s.popcnt(),
843            u32s.iter().map(|x| x.count_ones() as u64).sum()
844        );
845
846        let u64s: &[u64] = &[u64::MAX, 0x0F0F_0F0F_0F0F_0F0F, 0];
847        assert_eq!(
848            u64s.popcnt(),
849            u64s.iter().map(|x| x.count_ones() as u64).sum()
850        );
851
852        // Signed types and arrays resolve through the same impls (the doc
853        // example covers `Vec`).
854        let i32s = [-1i32, 0, 1, i32::MIN];
855        assert_eq!(
856            i32s.popcnt(),
857            i32s.iter().map(|x| x.count_ones() as u64).sum()
858        );
859        assert_eq!([u128::MAX, 0].popcnt(), 128);
860    }
861
862    /// Sweep every boundary-relevant size against the byte-wise reference using
863    /// a deterministic pseudo-random fill (xorshift). Covers tail handling,
864    /// the AVX2/AVX512 thresholds and multiple Harley-Seal outer iterations.
865    #[test]
866    fn pseudorandom_all_sizes() {
867        let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
868        let mut next = || {
869            state ^= state << 13;
870            state ^= state >> 7;
871            state ^= state << 17;
872            state
873        };
874
875        // Largest size + largest offset exercised below, plus margin. 4695
876        // bytes spans several 512-byte Harley-Seal iterations.
877        const MAX_SIZE: usize = 4695;
878        const MAX_OFF: usize = 7;
879        let mut bytes = vec![0u8; MAX_SIZE + MAX_OFF + 1];
880        for b in bytes.iter_mut() {
881            *b = (next() & 0xFF) as u8;
882        }
883
884        // Every size from 0 up through the AVX2/AVX512 active range, plus a few
885        // larger ones, exercised at multiple start offsets so alignment varies.
886        let sizes =
887            (0usize..=600).chain([1023, 1024, 1025, 2048, 4095, 4096, 4097, 4608, MAX_SIZE]);
888        for size in sizes {
889            for &off in &[0usize, 1, 3, MAX_OFF] {
890                let slice = &bytes[off..off + size];
891                assert_eq!(popcnt(slice), reference(slice), "size={size} off={off}");
892            }
893        }
894    }
895
896    /// Verify `popcnt()` of every suffix `bytes[i..]` against an independent
897    /// byte-wise reference, covering every length and a range of start
898    /// alignments in one sweep.
899    ///
900    /// Size defaults to 20_000 to keep `cargo test` fast — the sweep is O(n²) in
901    /// the work `popcnt` performs. Override with `SIMD_POPCNT_TEST_SIZE` for a
902    /// heavier run, e.g. `SIMD_POPCNT_TEST_SIZE=100000 cargo test --release suffix_sweep`.
903    #[test]
904    fn suffix_sweep() {
905        let size = std::env::var("SIMD_POPCNT_TEST_SIZE")
906            .ok()
907            .and_then(|s| s.parse::<usize>().ok())
908            .unwrap_or(20_000);
909
910        // All-ones array.
911        let ones = vec![0xFFu8; size];
912        check_all_suffixes(&ones);
913
914        // Deterministic pseudo-random array (fixed seed → reproducible failures).
915        let mut state: u64 = 0x2545_F491_4F6C_DD1D;
916        let mut bytes = vec![0u8; size];
917        for b in bytes.iter_mut() {
918            state ^= state << 13;
919            state ^= state >> 7;
920            state ^= state << 17;
921            *b = state as u8;
922        }
923        check_all_suffixes(&bytes);
924    }
925
926    /// Assert `popcnt(&bytes[i..])` for every `i` against an O(1) prefix-sum
927    /// reference, so only `popcnt` itself does O(n) work per suffix.
928    fn check_all_suffixes(bytes: &[u8]) {
929        let total: u64 = bytes.iter().map(|&b| popcnt64_bitwise(b as u64)).sum();
930        let mut prefix = 0u64; // popcount of bytes[..i]
931        for (i, &byte) in bytes.iter().enumerate() {
932            assert_eq!(popcnt(&bytes[i..]), total - prefix, "suffix at offset {i}");
933            prefix += popcnt64_bitwise(byte as u64);
934        }
935        // Empty suffix.
936        assert_eq!(popcnt(&bytes[bytes.len()..]), 0);
937    }
938}