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