Conversation
…evel PEXT The IndexIterator/Indices paths in filter_bits walked the precomputed indices Vec (up to 256 KB at 50% selectivity) to read the null bitmap one byte at a time via get_bit_raw. This caused severe cache pressure against the values buffer being filtered simultaneously. Replace with gather_bits: zip 64-bit chunks from the filter and source bitmaps, apply software PEXT to extract selected bits per chunk. This works directly on the two 8 KB bitmaps, eliminating the indices Vec traversal and reducing source reads from O(count) byte loads to O(filter_len/64) u64 loads. A threshold (count * 64 < filter_len) keeps the original indices path for very sparse selections where the tiny precomputed Vec is cheaper to walk than scanning all filter chunks. Benchmark delta on existing NULLs benchmarks (65536 elements): i32 w NULLs kept 1/2: 149.9 µs -> 38.1 µs (-74%) i32 w NULLs high selectivity: 17.9 µs -> 6.6 µs (-63%) i32 w NULLs low selectivity: 499 ns -> 276 ns (-60%) u8 w NULLs kept 1/2: 159.0 µs -> 63.4 µs (-75%) u8 w NULLs high selectivity: -- -> 2.8 µs (-58%) u8 w NULLs low selectivity: -- -> 234 ns (-35%)
|
run benchmark filter_kernels |
|
Hi @sdf-jkl, your benchmark configuration could not be parsed (#11086 (comment)). Error: Usage: Any benchmark name is accepted: Per-side configuration ( env:
# shared env is inherited by BOTH the build and the run, so build
# flags go here. Builds default to no debuginfo for speed; opt back
# in for hung-job gdb dumps and cap jobs to stay within memory:
CARGO_PROFILE_RELEASE_DEBUG: "1"
CARGO_BUILD_JOBS: "1"
baseline:
ref: v45.0.0
env:
# per-side env only reaches the benchmark run, not the build
DATAFUSION_RUNTIME_MEMORY_LIMIT: 1G
changed:
ref: v46.0.0
env:
DATAFUSION_RUNTIME_MEMORY_LIMIT: 2GFile an issue against this benchmark runner |
|
run benchmark filter_kernels |
|
🤖 Arrow criterion benchmark running (GKE) | trigger CPU Details (lscpu)Comparing perf/filter-bits-batched-extraction (2b11fc5) to 2078680 (merge-base) diff Run configurationrun benchmark filter_kernels
env:
BENCH_FILTER: "NULL"BENCH_COMMAND=cargo bench --features=arrow,async,test_common,experimental,object_store --bench filter_kernels File an issue against this benchmark runner |
|
🤖 Arrow criterion benchmark completed (GKE) | trigger Instance: Comparing perf/filter-bits-batched-extraction (2b11fc5) to 2078680 (merge-base) diff Run configurationrun benchmark filter_kernels
env:
BENCH_FILTER: "NULL"CPU Details (lscpu)Details
Resource Usagebase (merge-base)
branch
File an issue against this benchmark runner |
Which issue does this PR close?
Related to #11055. This draft builds on Richard's changes at
4727e6a654d28f6371a9fd352a34d8a77af9c0e8and includes those commits. The measurements below compare against that PR head, not againstmain.Rationale for this change
Filtering a bitmap requires gathering the selected bits from each input word and concatenating the resulting fragments. Finishing one word's variable-length extraction loop before starting the next leaves this work scalar in the inspected builds.
This draft processes eight words together, advancing one selected bit per active lane per iteration. It exposes independent operations across words to LLVM's vectorizer while keeping the implementation in stable, portable Rust. A batch runs until its largest mask population count is exhausted, so shorter lanes can do idle work; the sparse tradeoff still needs evaluation.
What changes are included in this PR?
u64fragments by testing each mask's lowest set bit and conditionally OR-ing a shared destination bit into the lane's result. Empty masks naturally contribute zero.pext64for leftover words and the final partial word.The batch helper is kept as a separate optimization unit with
#[inline(never)]. There are no ISA intrinsics, CPU-feature dispatch branches, or unstable Rust APIs.Are these changes tested?
cargo test -p arrow-select --lib --release --offline: all 432 tests passed.git diff --checkpassed.Local exploratory measurements of full Boolean filtering, 65,536 rows, in microseconds (lower is better):
Ryzen AI 9 HX PRO 470; rustc 1.98.1 / LLVM 22.1.8. The native build uses
RUSTFLAGS="-C target-cpu=native"for the harness and dependencies; the generic build has no CPU override. Each build compares the implementations in one executable, pinned to CPU 2, with rotated order over 21 rounds and approximately 3 ms per variant per round. Numbers are median thread CPU time, including allocation and excluding input/predicate construction. Repeated reuses one bitmap; cycling uses 64 prebuilt bitmaps. These are exploratory harness results, not Criterion confidence intervals, and thread CPU time does not eliminate clock/cache interference.The generic repeated 10% case regresses. Very sparse direct-gather cases also remain a concern; optimized sparse filtering may bypass this kernel.
Inspection of the extraction loop found scalar conditional moves in the final generic x86 build and ZMM SIMD in the native build. Standalone Rust probes produce YMM SIMD with
target-cpu=x86-64-v3and NEON for both generic AArch64 andtarget-cpu=neoverse-v2. ARM results are code-generation checks only: no ARM performance measurements or inspection of the bot's compiled binary yet.Are there any user-facing changes?
Filtering results and public APIs are unchanged. This draft changes performance and needs further evaluation, particularly on the ARM benchmark runner and sparse workloads.