Skip to content

Commit 5cfc0b5

Browse files
perf(buffer): use fearless_simd for popcount (#10327)
Signed-off-by: Claude <noreply@anthropic.com>
1 parent c6e51ba commit 5cfc0b5

4 files changed

Lines changed: 57 additions & 143 deletions

File tree

‎Cargo.lock‎

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ divan = { package = "codspeed-divan-compat", version = "5.0.0" }
159159
enum-iterator = "2.0.0"
160160
env_logger = "0.11"
161161
fastlanes = { version = "0.7.0", features = ["runtime"] }
162+
fearless_simd = "1.0.0"
163+
fearless_simd_macros = "0.1.0"
162164
flatbuffers = "25.2.10"
163165
fsst-rs = "0.6.0"
164166
futures = { version = "0.3.31", default-features = false }
@@ -261,7 +263,7 @@ spatialbench-parquet = { package = "parquet", version = "56", features = [
261263
] }
262264
static_assertions = "1.1"
263265
strum = "0.28"
264-
syn = { version = "3.0.2", features = ["full"] }
266+
syn = { version = "3.0.4", features = ["full"] }
265267
sysinfo = "0.38.0"
266268
tabled = { version = "0.21.0", default-features = false }
267269
taffy = "0.12"

‎vortex-buffer/Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ allocator-api2 = { workspace = true }
2525
arrow-buffer = { workspace = true }
2626
bitvec = { workspace = true }
2727
bytes = { workspace = true }
28+
fearless_simd = { workspace = true }
29+
fearless_simd_macros = { workspace = true }
2830
itertools = { workspace = true }
2931
memmap2 = { workspace = true, optional = true }
3032
serde = { workspace = true, optional = true }
Lines changed: 33 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
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;
510
use vortex_error::VortexExpect;
611

7-
use crate::dispatch::CpuKernel;
8-
912
#[inline]
1013
pub 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]
7879
fn 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

Comments
 (0)