perf(filter): replace bit-at-a-time null bitmap filtering - #11055
perf(filter): replace bit-at-a-time null bitmap filtering #11055Rich-T-kid wants to merge 4 commits into
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 |
|
🤖 Arrow criterion benchmark running (GKE) | trigger CPU Details (lscpu)Comparing rich-t-kid/filter-bits-gather-optimization (75281be) 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 rich-t-kid/filter-bits-gather-optimization (75281be) 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 |
|
run benchmark filter_kernels |
1 similar comment
|
run benchmark filter_kernels |
|
🤖 Arrow criterion benchmark running (GKE) | trigger CPU Details (lscpu)Comparing rich-t-kid/filter-bits-gather-optimization (200598b) 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 running (GKE) | trigger CPU Details (lscpu)Comparing rich-t-kid/filter-bits-gather-optimization (200598b) 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 rich-t-kid/filter-bits-gather-optimization (200598b) 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 |
|
🤖 Arrow criterion benchmark completed (GKE) | trigger Instance: Comparing rich-t-kid/filter-bits-gather-optimization (200598b) 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 |
c275451 to
4727e6a
Compare
|
run benchmark filter_kernels |
1 similar comment
|
run benchmark filter_kernels |
|
@sdf-jkl could you take a look 🚀 |
|
🤖 Arrow criterion benchmark running (GKE) | trigger CPU Details (lscpu)Comparing rich-t-kid/filter-bits-gather-optimization (4727e6a) 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 running (GKE) | trigger CPU Details (lscpu)Comparing rich-t-kid/filter-bits-gather-optimization (4727e6a) 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 rich-t-kid/filter-bits-gather-optimization (4727e6a) 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 |
|
🤖 Arrow criterion benchmark completed (GKE) | trigger Instance: Comparing rich-t-kid/filter-bits-gather-optimization (4727e6a) 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 |
| // SAFETY: indices were derived from the filter predicate | ||
| let bits = indices.iter().map(|src_idx| unsafe { | ||
| bit_util::get_bit_raw(buffer.values().as_ptr(), *src_idx + offset) | ||
| }); | ||
| // SAFETY: `Vec::iter()` reports its size correctly |
There was a problem hiding this comment.
going to add these safety comments back in
|
I'll take a look this weekend |
Which issue does this PR close?
Rationale for this change
When filtering arrays with a null bitmap, the current code walks one bit at a time
What changes are included in this PR?
Replaces the bit-at-a-time null bitmap loop with a word-level approach: instead of reading one bit per selected row, we load 64 filter bits and 64 source bits at a time, extract only the bits where the filter is set, and pack them directly into the output buffer.
Are these changes tested?
yes, existing test
Are there any user-facing changes?
no