@@ -220,10 +220,14 @@ fn popcnt_x86(bytes: &[u8]) -> u64 {
220220 {
221221 let mut cnt = 0u64 ;
222222 let mut rest = bytes;
223- // AVX2 only wins for arrays >= 512 bytes .
224- if bytes. len ( ) >= 512 {
223+ // A plain `popcnt256` loop for the medium range, Harley-Seal from ~1 KB .
224+ if bytes. len ( ) >= 64 {
225225 let n = bytes. len ( ) / 32 * 32 ;
226- cnt += unsafe { popcnt_avx2 ( & bytes[ ..n] ) } ;
226+ cnt += if bytes. len ( ) >= 1024 {
227+ unsafe { popcnt_avx2 ( & bytes[ ..n] ) }
228+ } else {
229+ unsafe { popcnt_avx2_medium ( & bytes[ ..n] ) }
230+ } ;
227231 rest = & bytes[ n..] ;
228232 }
229233 cnt + popcnt_scalar_static ( rest)
@@ -310,10 +314,17 @@ fn popcnt_x86_runtime(bytes: &[u8]) -> u64 {
310314 let mut cnt = 0u64 ;
311315 let mut rest = bytes;
312316
313- // AVX2 only wins for arrays >= 512 bytes.
314- if bytes. len ( ) >= 512 && is_x86_feature_detected ! ( "avx2" ) {
317+ // AVX2: a plain `popcnt256` loop for the medium range, Harley-Seal once it
318+ // pays off. The `popcnt256` loop beats scalar from ~64 bytes and beats
319+ // Harley-Seal until ~1 KB β the lookup-vs-Harley-Seal crossover across
320+ // Haswell..Cascadelake in the sse-popcount benchmarks is 1..2 KB.
321+ if bytes. len ( ) >= 64 && is_x86_feature_detected ! ( "avx2" ) {
315322 let n = bytes. len ( ) / 32 * 32 ;
316- cnt += unsafe { popcnt_avx2 ( & bytes[ ..n] ) } ;
323+ cnt += if bytes. len ( ) >= 1024 {
324+ unsafe { popcnt_avx2 ( & bytes[ ..n] ) }
325+ } else {
326+ unsafe { popcnt_avx2_medium ( & bytes[ ..n] ) }
327+ } ;
317328 rest = & bytes[ n..] ;
318329 }
319330
@@ -510,6 +521,44 @@ fn popcnt_avx2(bytes: &[u8]) -> u64 {
510521 lanes[ 0 ] + lanes[ 1 ] + lanes[ 2 ] + lanes[ 3 ]
511522}
512523
524+ /// Plain 2-accumulator `popcnt256` loop for medium arrays (~64 bytes to ~1 KB).
525+ /// Harley-Seal's fixed CSA-reduction epilogue makes it lose to just running
526+ /// `popcnt256` in a loop until ~1 KB, and this beats the scalar path from ~64
527+ /// bytes up. `bytes.len()` must be a multiple of 32.
528+ #[ cfg( all(
529+ any( target_arch = "x86" , target_arch = "x86_64" ) ,
530+ not( target_feature = "avx512vpopcntdq" ) ,
531+ any( target_feature = "avx2" , feature = "std" )
532+ ) ) ]
533+ #[ target_feature( enable = "avx2" ) ]
534+ #[ inline]
535+ fn popcnt_avx2_medium ( bytes : & [ u8 ] ) -> u64 {
536+ let mut acc0 = _mm256_setzero_si256 ( ) ;
537+ let mut acc1 = _mm256_setzero_si256 ( ) ;
538+
539+ let ( pairs, tail) = bytes. as_chunks :: < 64 > ( ) ;
540+ for chunk in pairs {
541+ let p = chunk. as_ptr ( ) . cast :: < __m256i > ( ) ;
542+ // SAFETY: `chunk` is 64 bytes, so both 32-byte loads are in bounds.
543+ unsafe {
544+ acc0 = _mm256_add_epi64 ( acc0, popcnt256 ( _mm256_loadu_si256 ( p. add ( 0 ) ) ) ) ;
545+ acc1 = _mm256_add_epi64 ( acc1, popcnt256 ( _mm256_loadu_si256 ( p. add ( 1 ) ) ) ) ;
546+ }
547+ }
548+
549+ // 0 or 1 leftover 32-byte vector (`bytes.len()` is a multiple of 32).
550+ let ( vecs, _) = tail. as_chunks :: < 32 > ( ) ;
551+ for chunk in vecs {
552+ let v = unsafe { _mm256_loadu_si256 ( chunk. as_ptr ( ) . cast :: < __m256i > ( ) ) } ;
553+ acc0 = _mm256_add_epi64 ( acc0, popcnt256 ( v) ) ;
554+ }
555+
556+ let cnt = _mm256_add_epi64 ( acc0, acc1) ;
557+ // SAFETY: `__m256i` and `[u64; 4]` are both 32 bytes with no invalid bit patterns.
558+ let lanes: [ u64 ; 4 ] = unsafe { core:: mem:: transmute ( cnt) } ;
559+ lanes[ 0 ] + lanes[ 1 ] + lanes[ 2 ] + lanes[ 3 ]
560+ }
561+
513562// ββ AVX512 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
514563
515564/// AVX512-VPOPCNTDQ population count, handling any length: a 4Γ-unrolled
0 commit comments