11// SPDX-License-Identifier: Apache-2.0
22// SPDX-FileCopyrightText: Copyright the Vortex contributors
33
4- #[ cfg( target_arch = "x86_64" ) ]
4+ use fearless_simd:: Level ;
5+ use fearless_simd:: Simd ;
6+ use fearless_simd:: prelude:: * ;
7+ use fearless_simd:: u8x64;
8+ use fearless_simd:: u64x8;
9+ use fearless_simd_macros:: simd;
510use vortex_error:: VortexExpect ;
611
7- use crate :: dispatch:: CpuKernel ;
8-
912#[ inline]
1013pub fn count_ones ( bytes : & [ u8 ] , offset : usize , len : usize ) -> usize {
1114 if bytes. is_empty ( ) {
@@ -72,67 +75,16 @@ fn mask_byte(byte: u8, bit_offset: usize, bit_len: usize) -> u8 {
7275 shifted & mask
7376}
7477
75- type CountOnes = unsafe fn ( & [ u8 ] ) -> usize ;
76-
7778#[ inline]
7879fn count_ones_aligned ( bytes : & [ u8 ] ) -> usize {
7980 // SIMD kernels only pay off from 32 bytes. Below that, call the scalar kernel
80- // directly: it stays inlinable and skips the dispatch indirection , which would
81- // otherwise dominate the couple of word popcounts.
81+ // directly: it stays inlinable and skips the dispatch, which would otherwise
82+ // dominate the couple of word popcounts.
8283 if bytes. len ( ) < 32 {
8384 return count_ones_aligned_scalar ( bytes) ;
8485 }
8586
86- static KERNEL : CpuKernel < CountOnes > = CpuKernel :: new ( || {
87- #[ cfg( target_arch = "x86_64" ) ]
88- {
89- if is_x86_feature_detected ! ( "avx512f" )
90- && is_x86_feature_detected ! ( "avx512vpopcntdq" )
91- && is_x86_feature_detected ! ( "avx2" )
92- {
93- return count_ones_aligned_avx512_any_len;
94- }
95- if is_x86_feature_detected ! ( "avx2" ) {
96- return count_ones_aligned_avx2_any_len;
97- }
98- }
99- count_ones_aligned_scalar
100- } ) ;
101- // SAFETY: the selector only returns kernels that are safe or whose required CPU
102- // features were probed before selection.
103- unsafe { KERNEL . get ( ) ( bytes) }
104- }
105-
106- /// Length-aware AVX-512 entry: SIMD only pays off from 64 (AVX-512) / 32 (AVX2) bytes.
107- ///
108- /// # Safety
109- /// Requires AVX-512F, AVX-512VPOPCNTDQ, and AVX2.
110- #[ cfg( target_arch = "x86_64" ) ]
111- #[ target_feature( enable = "avx512f,avx512vpopcntdq,avx2" ) ]
112- unsafe fn count_ones_aligned_avx512_any_len ( bytes : & [ u8 ] ) -> usize {
113- if bytes. len ( ) >= 64 {
114- // SAFETY: the caller guarantees the required target features.
115- return unsafe { count_ones_aligned_avx512 ( bytes) } ;
116- }
117- if bytes. len ( ) >= 32 {
118- // SAFETY: every AVX-512F CPU also supports AVX2.
119- return unsafe { count_ones_aligned_avx2 ( bytes) } ;
120- }
121- count_ones_aligned_scalar ( bytes)
122- }
123-
124- /// Length-aware AVX2 entry: SIMD only pays off from 32 bytes.
125- ///
126- /// # Safety
127- /// Requires AVX2.
128- #[ cfg( target_arch = "x86_64" ) ]
129- #[ target_feature( enable = "avx2" ) ]
130- unsafe fn count_ones_aligned_avx2_any_len ( bytes : & [ u8 ] ) -> usize {
131- if bytes. len ( ) >= 32 {
132- // SAFETY: the caller guarantees AVX2.
133- return unsafe { count_ones_aligned_avx2 ( bytes) } ;
134- }
135- count_ones_aligned_scalar ( bytes)
87+ fearless_simd:: dispatch!( Level :: new( ) , simd => count_ones_aligned_simd( simd, bytes) )
13688}
13789
13890#[ inline]
@@ -150,93 +102,18 @@ fn count_ones_aligned_scalar(bytes: &[u8]) -> usize {
150102 . sum :: < usize > ( )
151103}
152104
153- #[ cfg( target_arch = "x86_64" ) ]
154- #[ target_feature( enable = "avx2" ) ]
155- unsafe fn count_ones_aligned_avx2 ( bytes : & [ u8 ] ) -> usize {
156- use std:: arch:: x86_64:: __m256i;
157- use std:: arch:: x86_64:: _mm256_add_epi8;
158- use std:: arch:: x86_64:: _mm256_add_epi64;
159- use std:: arch:: x86_64:: _mm256_and_si256;
160- use std:: arch:: x86_64:: _mm256_loadu_si256;
161- use std:: arch:: x86_64:: _mm256_sad_epu8;
162- use std:: arch:: x86_64:: _mm256_set1_epi8;
163- use std:: arch:: x86_64:: _mm256_setr_epi8;
164- use std:: arch:: x86_64:: _mm256_setzero_si256;
165- use std:: arch:: x86_64:: _mm256_shuffle_epi8;
166- use std:: arch:: x86_64:: _mm256_srli_epi16;
167- use std:: arch:: x86_64:: _mm256_storeu_si256;
168-
169- #[ inline]
170- unsafe fn byte_popcount ( chunk : __m256i , mask : __m256i , lookup : __m256i ) -> __m256i {
171- let lo = unsafe { _mm256_and_si256 ( chunk, mask) } ;
172- let hi = unsafe { _mm256_and_si256 ( _mm256_srli_epi16 ( chunk, 4 ) , mask) } ;
173- unsafe {
174- _mm256_add_epi8 (
175- _mm256_shuffle_epi8 ( lookup, lo) ,
176- _mm256_shuffle_epi8 ( lookup, hi) ,
177- )
178- }
179- }
180-
181- let lookup = _mm256_setr_epi8 (
182- 0 , 1 , 1 , 2 , 1 , 2 , 2 , 3 , 1 , 2 , 2 , 3 , 2 , 3 , 3 , 4 , 0 , 1 , 1 , 2 , 1 , 2 , 2 , 3 , 1 , 2 , 2 , 3 , 2 , 3 ,
183- 3 , 4 ,
184- ) ;
185- let mask = _mm256_set1_epi8 ( 0x0f ) ;
186- let zero = _mm256_setzero_si256 ( ) ;
187- let mut accum = _mm256_setzero_si256 ( ) ;
188- let mut index = 0 ;
189-
190- while index + 128 <= bytes. len ( ) {
191- for lane in 0 ..4 {
192- let ptr = unsafe { bytes. as_ptr ( ) . add ( index + lane * 32 ) } . cast :: < __m256i > ( ) ;
193- let chunk = unsafe { _mm256_loadu_si256 ( ptr) } ;
194- let counts = unsafe { byte_popcount ( chunk, mask, lookup) } ;
195- accum = _mm256_add_epi64 ( accum, _mm256_sad_epu8 ( counts, zero) ) ;
196- }
197- index += 128 ;
198- }
199-
200- while index + 32 <= bytes. len ( ) {
201- let ptr = unsafe { bytes. as_ptr ( ) . add ( index) } . cast :: < __m256i > ( ) ;
202- let chunk = unsafe { _mm256_loadu_si256 ( ptr) } ;
203- let counts = unsafe { byte_popcount ( chunk, mask, lookup) } ;
204- accum = _mm256_add_epi64 ( accum, _mm256_sad_epu8 ( counts, zero) ) ;
205- index += 32 ;
206- }
207-
208- let mut lanes = [ 0u64 ; 4 ] ;
209- unsafe { _mm256_storeu_si256 ( lanes. as_mut_ptr ( ) . cast :: < __m256i > ( ) , accum) } ;
210-
211- usize:: try_from ( lanes. iter ( ) . sum :: < u64 > ( ) ) . vortex_expect ( "true_count doesn't fit in usize" )
212- + count_ones_aligned_scalar ( & bytes[ index..] )
213- }
214-
215- #[ cfg( target_arch = "x86_64" ) ]
216- #[ target_feature( enable = "avx512f,avx512vpopcntdq" ) ]
217- unsafe fn count_ones_aligned_avx512 ( bytes : & [ u8 ] ) -> usize {
218- use std:: arch:: x86_64:: __m512i;
219- use std:: arch:: x86_64:: _mm512_add_epi64;
220- use std:: arch:: x86_64:: _mm512_loadu_si512;
221- use std:: arch:: x86_64:: _mm512_popcnt_epi64;
222- use std:: arch:: x86_64:: _mm512_setzero_si512;
223- use std:: arch:: x86_64:: _mm512_storeu_si512;
224-
225- let mut accum = _mm512_setzero_si512 ( ) ;
226- let mut index = 0 ;
227-
228- while index + 64 <= bytes. len ( ) {
229- let ptr = unsafe { bytes. as_ptr ( ) . add ( index) } . cast :: < __m512i > ( ) ;
230- let chunk = unsafe { _mm512_loadu_si512 ( ptr) } ;
231- accum = _mm512_add_epi64 ( accum, _mm512_popcnt_epi64 ( chunk) ) ;
232- index += 64 ;
105+ /// Lane-wise `u64` popcount over 64-byte vectors. The total is independent of lane byte order.
106+ #[ simd]
107+ fn count_ones_aligned_simd < S : Simd > ( simd : S , bytes : & [ u8 ] ) -> usize {
108+ let ( chunks, tail) = bytes. as_chunks :: < 64 > ( ) ;
109+ let mut accum = u64x8:: splat ( simd, 0 ) ;
110+ for chunk in chunks {
111+ let words: u64x8 < S > = u8x64:: from_slice ( simd, chunk) . bitcast ( ) ;
112+ accum += words. count_ones ( ) ;
233113 }
234114
235- let mut lanes = [ 0u64 ; 8 ] ;
236- unsafe { _mm512_storeu_si512 ( lanes. as_mut_ptr ( ) . cast :: < __m512i > ( ) , accum) } ;
237-
238- usize:: try_from ( lanes. iter ( ) . sum :: < u64 > ( ) ) . vortex_expect ( "true_count doesn't fit in usize" )
239- + count_ones_aligned_scalar ( & bytes[ index..] )
115+ usize:: try_from ( accum. reduce_sum ( ) ) . vortex_expect ( "true_count doesn't fit in usize" )
116+ + count_ones_aligned_scalar ( tail)
240117}
241118
242119#[ cfg( test) ]
@@ -274,4 +151,18 @@ mod tests {
274151 "offset={offset} len={slice_len}"
275152 ) ;
276153 }
154+
155+ /// Lengths spanning many 64-byte SIMD chunks plus a scalar tail.
156+ #[ cfg_attr( miri, ignore) ]
157+ #[ rstest]
158+ fn test_count_ones_matches_iteration_multi_chunk (
159+ #[ values( 0usize , 5 ) ] offset : usize ,
160+ #[ values( 1024usize , 4096 , 4097 , 65_537 ) ] len : usize ,
161+ ) {
162+ let buf = BitBuffer :: collect_bool ( offset + len, |i| ( i % 3 == 0 ) ^ ( i % 7 == 0 ) ) ;
163+ let sliced = buf. slice ( offset..offset + len) ;
164+ let expected = sliced. iter ( ) . filter ( |bit| * bit) . count ( ) ;
165+
166+ assert_eq ! ( sliced. true_count( ) , expected, "offset={offset} len={len}" ) ;
167+ }
277168}
0 commit comments