Skip to content

perf: Optimize array_filter - #25754

Merged
neilconway merged 9 commits into
apache:mainfrom
neilconway:neilc/refactor-array-filter
Oct 16, 2026
Merged

neilconway merged 9 commits into
apache:mainfrom
neilconway:neilc/refactor-array-filter

Conversation

@neilconway

@neilconway neilconway commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

  • N/A

Rationale for this change

After applying the predicate to the input list, array_filter has to compute two things: the row offsets for the result list, and the "keep" bitmap to pass to Arrow's filter kernel to copy the result values.

When the predicate returned one or more NULLs, the previous implementation computed row offsets with a bit-by-bit loop over the result of applying the predicate, which is relatively slow. We can do better by first converting the predicate result bitmap into the "keep" bitmap (normalizing NULLs to cleared bits), and then using the keep bitmap to compute row offsets. This last step can be done in a single pass over the keep bitmap, using popcount on 64-bit words rather than a bit-by-bit loop.

Along the way, avoid creating two transient copies of the input row offset buffer. The previous code built a vector of adjusted row offsets to account for slicing of the input list; we can instead account for slicing without an additional data structure but instead by subtracting the first offset value from each row offset. This matches how sliced inputs are typically handled elsewhere.

Benchmarks: (M4 Max)

  • int32/keep10/no_nulls, 2.592 → 1.500 µs, −42.1%
  • int32/keep50/no_nulls, 3.381 → 2.350 µs, −30.5%
  • int32/keep90/no_nulls, 3.937 → 2.908 µs, −26.1%
  • int32/keep50/null_elements25, 6.075 → 3.069 µs, −49.5%
  • int32/keep_null_elements, 2.882 → 1.850 µs, −35.8%
  • int32/keep50/null_rows50, 3.913 → 2.649 µs, −32.3%
  • int32/keep50/null_rows99, 0.887 → 0.910 µs, +2.6%
  • utf8/keep10/no_nulls, 10.066 → 9.021 µs, −10.4%
  • utf8/keep50/no_nulls, 17.283 → 16.163 µs, −6.5%
  • utf8/keep90/no_nulls, 13.333 → 12.291 µs, −7.8%
  • utf8/keep50/null_elements25, 19.145 → 16.035 µs, −16.2%
  • utf8/keep_null_elements, 4.276 → 3.230 µs, −24.5%
  • utf8/keep50/null_rows50, 10.165 → 8.924 µs, −12.2%
  • utf8/keep50/null_rows99, 0.939 → 0.956 µs, +1.8%
  • utf8_view/keep10/no_nulls, 5.142 → 4.109 µs, −20.1%
  • utf8_view/keep50/no_nulls, 6.160 → 5.118 µs, −16.9%
  • utf8_view/keep90/no_nulls, 6.899 → 5.837 µs, −15.4%
  • utf8_view/keep50/null_elements25, 9.254 → 6.166 µs, −33.4%
  • utf8_view/keep_null_elements, 3.007 → 1.968 µs, −34.6%
  • utf8_view/keep50/null_rows50, 5.572 → 4.294 µs, −22.9%
  • utf8_view/keep50/null_rows99, 0.926 → 0.948 µs, +2.4%
  • utf8/wide_128, 16.476 → 16.268 µs, −1.3%
  • utf8/variable_0_to_32, 17.254 → 16.081 µs, −6.8%
  • int32/large_1m/null_elements25, 3010.900 → 779.340 µs, −74.1%
  • int32/narrow_2/null_elements25, 8.154 → 4.102 µs, −49.7%
  • scalar/true, 0.325 → 0.271 µs, −16.5%
  • scalar/false, 0.488 → 0.436 µs, −10.7%
  • scalar/null, 0.485 → 0.419 µs, −13.6%
  • int32/keep_all, 2.299 → 1.252 µs, −45.6%
  • int32/keep_none, 2.376 → 1.356 µs, −42.9%
  • int32/captured_column, 4.830 → 3.838 µs, −20.5%
  • int32/keep50/null_rows50/hidden_storage, 5.091 → 3.833 µs, −24.7%
  • utf8/sliced, 19.148 → 16.247 µs, −15.2%
  • utf8/large_list, 19.142 → 16.190 µs, −15.4%

What changes are included in this PR?

  • Optimize computing row offsets for result, when predicate returns NULLs
  • Compute row offsets in a simpler pass when the input list has no NULL rows
  • Avoid needless copy/allocation for adjusted input row offsets
  • Add fast-paths for when the predicate selects all elements or no elements
  • Refactor List and LargeList into a single implementation, parameterized on offset type
  • Add benchmark for array_filter

What is the testing strategy for this PR?

Existing tests pass, test coverage expanded.

Are there any user-facing changes?

No.

Measure a planned array_filter projection, including lambda evaluation and
normalization of NULL list rows, with setup outside the timed loop.

Cover common element types, predicate selectivity and NULLs, list shapes,
scalar and array no-ops, captured columns, slices, and LargeList. Include
both empty NULL rows and NULL rows retaining hidden elements. Check results
against an independent element selection before timing each case.
Use one generic implementation for List and LargeList, including scalar
predicate handling. Read original row offsets relative to the visible
values instead of rebuilding an intermediate offset buffer, and return the
original input when all visible values are retained.

Compute adjusted row boundaries only for consumers that need them.
array_first and array_any_match request them once before iterating;
array_filter no longer allocates this unused vector.
Normalize NULL predicate results once, then use the same keep bitmap to
count retained elements per row and select output values. Copy the bitmap
only when a NULL list row stores elements, and bypass the filter kernel
when no elements are retained.

Keep NULL elements selected by array_filter predicates. Preserve the caller's
existing NULL-row normalization and lambda evaluation behavior. Add tests
for slices, NULL elements and rows, field metadata, buffer reuse, and sliced
output capacity. Check physical buffer sharing through array wrappers.
@github-actions github-actions Bot added sqllogictest SQL Logic Tests (.slt) functions Changes to functions implementation labels Sep 25, 2026
@neilconway

Copy link
Copy Markdown
Contributor Author

This could be refactored to share some code with array_compact (e.g., computing result row offsets from the validity bitmap), but when I tried that, it was hard to get optimal performance for both call-sites, so I'll defer that for now.

@neilconway

Copy link
Copy Markdown
Contributor Author

FYI @saadtajwar @comphead

@codecov-commenter

codecov-commenter commented Sep 25, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.27089% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.88%. Comparing base (cf4869c) to head (8052059).
⚠️ Report is 16 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/functions-nested/src/array_filter.rs 97.46% 1 Missing and 3 partials ⚠️
datafusion/functions-nested/src/array_any_match.rs 66.66% 0 Missing and 1 partial ⚠️
datafusion/functions-nested/src/array_first.rs 80.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #25754      +/-   ##
==========================================
+ Coverage   82.84%   82.88%   +0.03%     
==========================================
  Files        1151     1153       +2     
  Lines      455961   457297    +1336     
  Branches   455961   457297    +1336     
==========================================
+ Hits       377760   379030    +1270     
- Misses      55245    55264      +19     
- Partials    22956    23003      +47     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@kosiew

kosiew commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

run benchmark array_filter

baseline:
  ref: "f721b9567f"

@adriangbot

Copy link
Copy Markdown

Benchmark for this request failed before finishing (Kubernetes reason: BackoffLimitExceeded).

Benchmarks requested: array_filter

Runner log (last 40 lines)
2026-10-15T10:29:04.262350Z  INFO runner starting benchmark runner bench_type=Datafusion, pr_url=https://github.com/apache/datafusion/pull/25754, benchmarks=array_filter
2026-10-15T10:29:04.336973Z  INFO benchmark_controller::runner::bench_datafusion === Cloning PR branch ===
2026-10-15T10:29:04.336992Z  INFO benchmark_controller::runner::shell running command cmd=git, args=["clone", "--depth=200", "https://github.com/apache/datafusion.git", "/workspace/datafusion-branch"], cwd="/"
2026-10-15T10:29:09.347408Z  INFO benchmark_controller::runner::shell running command cmd=git, args=["fetch", "origin", "refs/pull/25754/head:neilc/refactor-array-filter", "main"], cwd="/workspace/datafusion-branch"
2026-10-15T10:29:29.354183Z  INFO benchmark_controller::runner::shell running command cmd=git, args=["checkout", "neilc/refactor-array-filter"], cwd="/workspace/datafusion-branch"
2026-10-15T10:29:34.355484Z  INFO benchmark_controller::runner::shell running command cmd=git, args=["merge-base", "HEAD", "origin/main"], cwd="/workspace/datafusion-branch"
2026-10-15T10:29:39.357834Z ERROR runner benchmark failed error=git merge-base: git merge-base HEAD origin/main exited with code 1
stdout:

stderr:
Kubernetes message
Job has reached the specified backoff limit

File an issue against this benchmark runner

@kosiew

kosiew commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

run benchmark array_filter

baseline:
  ref: "f721b9567f"

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c6190093882-3437-h222x 6.12.94+ #1 SMP Fri Aug 21 08:00:16 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing neilc/refactor-array-filter (39d794d) to f721b95 diff

Run configuration
run benchmark array_filter
baseline:
  ref: "f721b9567f"

Results will be posted here when complete


File an issue against this benchmark runner

@kosiew kosiew left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@neilconway,

Thanks for this optimization. The changes address the issue requirements by normalizing nullable predicates, using popcount to compute result offsets, and avoiding unnecessary offset and bitmap allocations. I also checked sliced inputs, NULL list rows with hidden elements, retained NULL values, and the existing fast paths. I found no blocking issues.

I have two optional suggestions for improving test and benchmark coverage. Neither needs to block merging.

LGTM!

Comment thread datafusion/functions-nested/src/utils.rs Outdated
Comment thread datafusion/functions-nested/benches/array_filter.rs
@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

Comparing neilc/refactor-array-filter (39d794d) to f721b95 diff

Run configuration
run benchmark array_filter
baseline:
  ref: "f721b9567f"
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                                   HEAD                                   neilc_refactor-array-filter
-----                                                   ----                                   ---------------------------
array_filter/int32/captured_column                      1.38     12.8±0.07µs        ? ?/sec    1.00      9.3±0.16µs        ? ?/sec
array_filter/int32/keep10/no_nulls                      1.63      9.1±0.06µs        ? ?/sec    1.00      5.6±0.17µs        ? ?/sec
array_filter/int32/keep50/no_nulls                      1.51     10.4±0.05µs        ? ?/sec    1.00      6.9±0.17µs        ? ?/sec
array_filter/int32/keep50/null_elements25               1.18     10.2±0.02µs        ? ?/sec    1.00      8.6±0.16µs        ? ?/sec
array_filter/int32/keep50/null_rows50                   1.13      7.0±0.01µs        ? ?/sec    1.00      6.2±0.02µs        ? ?/sec
array_filter/int32/keep50/null_rows50/hidden_storage    1.09      9.5±0.02µs        ? ?/sec    1.00      8.7±0.01µs        ? ?/sec
array_filter/int32/keep50/null_rows99                   1.00      2.2±0.01µs        ? ?/sec    1.00      2.2±0.02µs        ? ?/sec
array_filter/int32/keep90/no_nulls                      1.54     10.1±0.06µs        ? ?/sec    1.00      6.6±0.16µs        ? ?/sec
array_filter/int32/keep_all                             1.72      8.8±0.06µs        ? ?/sec    1.00      5.1±0.16µs        ? ?/sec
array_filter/int32/keep_none                            1.63      9.1±0.06µs        ? ?/sec    1.00      5.6±0.16µs        ? ?/sec
array_filter/int32/keep_null_elements                   1.56     10.0±0.06µs        ? ?/sec    1.00      6.4±0.16µs        ? ?/sec
array_filter/int32/large_1m/null_elements25             2.20      4.2±0.01ms        ? ?/sec    1.00  1928.0±43.90µs        ? ?/sec
array_filter/scalar/false                               1.09   1195.6±8.21ns        ? ?/sec    1.00   1099.9±7.32ns        ? ?/sec
array_filter/scalar/null                                1.11  1221.1±14.73ns        ? ?/sec    1.00   1104.1±9.86ns        ? ?/sec
array_filter/scalar/true                                1.15   735.8±10.61ns        ? ?/sec    1.00    637.6±4.78ns        ? ?/sec
array_filter/utf8/keep10/no_nulls                       1.20     21.1±0.07µs        ? ?/sec    1.00     17.6±0.15µs        ? ?/sec
array_filter/utf8/keep50/no_nulls                       1.12     30.5±0.21µs        ? ?/sec    1.00     27.3±0.12µs        ? ?/sec
array_filter/utf8/keep50/null_elements25                1.04     27.5±0.15µs        ? ?/sec    1.00     26.5±0.12µs        ? ?/sec
array_filter/utf8/keep50/null_rows50                    1.06     15.0±0.07µs        ? ?/sec    1.00     14.2±0.02µs        ? ?/sec
array_filter/utf8/keep50/null_rows99                    1.02      2.3±0.01µs        ? ?/sec    1.00      2.3±0.01µs        ? ?/sec
array_filter/utf8/keep90/no_nulls                       1.12     29.3±0.07µs        ? ?/sec    1.00     26.2±0.17µs        ? ?/sec
array_filter/utf8/keep_null_elements                    1.29     13.1±0.07µs        ? ?/sec    1.00     10.1±0.17µs        ? ?/sec
array_filter/utf8/large_list                            1.06     27.7±0.12µs        ? ?/sec    1.00     26.1±0.23µs        ? ?/sec
array_filter/utf8/sliced                                1.07     27.3±0.13µs        ? ?/sec    1.00     25.7±0.15µs        ? ?/sec
array_filter/utf8/variable_0_to_32                      1.10     29.2±0.14µs        ? ?/sec    1.00     26.6±0.16µs        ? ?/sec
array_filter/utf8/wide_128                              1.00     24.4±0.26µs        ? ?/sec    1.01     24.8±0.13µs        ? ?/sec
array_filter/utf8_view/keep10/no_nulls                  1.35     13.4±0.06µs        ? ?/sec    1.00      9.9±0.16µs        ? ?/sec
array_filter/utf8_view/keep50/no_nulls                  1.29     15.0±0.06µs        ? ?/sec    1.00     11.6±0.17µs        ? ?/sec
array_filter/utf8_view/keep50/null_elements25           1.12     15.3±0.01µs        ? ?/sec    1.00     13.6±0.17µs        ? ?/sec
array_filter/utf8_view/keep50/null_rows50               1.09      9.6±0.02µs        ? ?/sec    1.00      8.8±0.02µs        ? ?/sec
array_filter/utf8_view/keep50/null_rows99               1.00      2.3±0.01µs        ? ?/sec    1.00      2.3±0.01µs        ? ?/sec
array_filter/utf8_view/keep90/no_nulls                  1.32     14.7±0.06µs        ? ?/sec    1.00     11.2±0.16µs        ? ?/sec
array_filter/utf8_view/keep_null_elements               1.53     10.1±0.07µs        ? ?/sec    1.00      6.6±0.16µs        ? ?/sec

Resource Usage

array_filter — base (merge-base)

Metric Value
Wall time 650.1s
Peak memory 62.5 MiB
Avg memory 20.3 MiB
CPU user 383.3s
CPU sys 0.3s
Peak spill 0 B

array_filter — branch

Metric Value
Wall time 630.1s
Peak memory 54.4 MiB
Avg memory 18.7 MiB
CPU user 379.8s
CPU sys 0.3s
Peak spill 0 B

File an issue against this benchmark runner

Counting each row's kept elements with `count_set_bits_offset` has a fixed
cost per call, which made lists of very short rows slower than on main.
`prepare_list_filter` now reads the keep bitmap's 64-bit words once while
keeping a running count of set bits, so each valid row costs one mask and
one popcount. Empty NULL rows are skipped, and NULL rows that store elements
subtract their set bits from the count.

Also add an `int32/narrow_2/null_elements25` benchmark case (2,048 rows of
2 elements), test `prepare_list_filter` with rows that span 64-bit words,
and test the counter directly.

Benchmarks vs main (M4 Max):

  - int32/narrow_2/null_elements25, 8.38 -> 7.04 us, -16% (was +15%)
  - int32/keep50/null_elements25, 6.26 -> 3.55 us, -43%
  - int32/large_1m/null_elements25, 3120 -> 886 us, -72%
  - int32/keep50/null_rows50/hidden_storage, 5.29 -> 3.94 us, -26%
@neilconway

Copy link
Copy Markdown
Contributor Author

Thanks for the review, @kosiew !

@neilconway
neilconway enabled auto-merge October 15, 2026 15:23
@neilconway

Copy link
Copy Markdown
Contributor Author

run benchmark array_filter

baseline:
  ref: "f721b9567f"

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c6195802799-3467-pzd66 6.12.94+ #1 SMP Fri Aug 21 08:00:16 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing neilc/refactor-array-filter (021c8fd) to f721b95 diff

Run configuration
run benchmark array_filter
baseline:
  ref: "f721b9567f"

Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

Comparing neilc/refactor-array-filter (021c8fd) to f721b95 diff

Run configuration
run benchmark array_filter
baseline:
  ref: "f721b9567f"
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                                   HEAD                                   neilc_refactor-array-filter
-----                                                   ----                                   ---------------------------
array_filter/int32/captured_column                      1.47     12.8±0.06µs        ? ?/sec    1.00      8.7±0.02µs        ? ?/sec
array_filter/int32/keep10/no_nulls                      1.81      9.1±0.06µs        ? ?/sec    1.00      5.0±0.00µs        ? ?/sec
array_filter/int32/keep50/no_nulls                      1.64     10.5±0.06µs        ? ?/sec    1.00      6.4±0.01µs        ? ?/sec
array_filter/int32/keep50/null_elements25               1.27     10.2±0.01µs        ? ?/sec    1.00      8.0±0.01µs        ? ?/sec
array_filter/int32/keep50/null_rows50                   1.19      7.0±0.01µs        ? ?/sec    1.00      5.9±0.01µs        ? ?/sec
array_filter/int32/keep50/null_rows50/hidden_storage    1.13      9.5±0.03µs        ? ?/sec    1.00      8.4±0.01µs        ? ?/sec
array_filter/int32/keep50/null_rows99                   1.00      2.2±0.01µs        ? ?/sec    1.01      2.2±0.01µs        ? ?/sec
array_filter/int32/keep90/no_nulls                      1.68     10.1±0.06µs        ? ?/sec    1.00      6.0±0.01µs        ? ?/sec
array_filter/int32/keep_all                             1.94      8.8±0.06µs        ? ?/sec    1.00      4.5±0.01µs        ? ?/sec
array_filter/int32/keep_none                            1.78      9.1±0.06µs        ? ?/sec    1.00      5.1±0.01µs        ? ?/sec
array_filter/int32/keep_null_elements                   1.70      9.9±0.06µs        ? ?/sec    1.00      5.8±0.00µs        ? ?/sec
array_filter/int32/large_1m/null_elements25             2.36      4.3±0.01ms        ? ?/sec    1.00   1801.5±5.70µs        ? ?/sec
array_filter/int32/narrow_2/null_elements25                                                    1.00     24.3±0.01µs        ? ?/sec
array_filter/scalar/false                               1.12   1232.6±4.81ns        ? ?/sec    1.00   1098.5±8.58ns        ? ?/sec
array_filter/scalar/null                                1.10   1211.0±9.51ns        ? ?/sec    1.00   1097.9±7.66ns        ? ?/sec
array_filter/scalar/true                                1.13    731.9±2.68ns        ? ?/sec    1.00    645.3±3.05ns        ? ?/sec
array_filter/utf8/keep10/no_nulls                       1.23     21.0±0.08µs        ? ?/sec    1.00     17.1±0.02µs        ? ?/sec
array_filter/utf8/keep50/no_nulls                       1.13     29.6±0.14µs        ? ?/sec    1.00     26.3±0.09µs        ? ?/sec
array_filter/utf8/keep50/null_elements25                1.05     27.8±0.06µs        ? ?/sec    1.00     26.3±0.16µs        ? ?/sec
array_filter/utf8/keep50/null_rows50                    1.06     14.9±0.03µs        ? ?/sec    1.00     14.1±0.03µs        ? ?/sec
array_filter/utf8/keep50/null_rows99                    1.00      2.3±0.01µs        ? ?/sec    1.01      2.3±0.00µs        ? ?/sec
array_filter/utf8/keep90/no_nulls                       1.14     29.4±0.07µs        ? ?/sec    1.00     25.8±0.04µs        ? ?/sec
array_filter/utf8/keep_null_elements                    1.45     13.0±0.07µs        ? ?/sec    1.00      9.0±0.01µs        ? ?/sec
array_filter/utf8/large_list                            1.09     27.8±0.06µs        ? ?/sec    1.00     25.6±0.14µs        ? ?/sec
array_filter/utf8/sliced                                1.08     27.7±0.07µs        ? ?/sec    1.00     25.5±0.08µs        ? ?/sec
array_filter/utf8/variable_0_to_32                      1.16     29.6±0.17µs        ? ?/sec    1.00     25.6±0.10µs        ? ?/sec
array_filter/utf8/wide_128                              1.01     24.2±0.13µs        ? ?/sec    1.00     24.0±0.09µs        ? ?/sec
array_filter/utf8_view/keep10/no_nulls                  1.43     13.5±0.06µs        ? ?/sec    1.00      9.4±0.01µs        ? ?/sec
array_filter/utf8_view/keep50/no_nulls                  1.40     15.0±0.06µs        ? ?/sec    1.00     10.7±0.01µs        ? ?/sec
array_filter/utf8_view/keep50/null_elements25           1.17     15.2±0.01µs        ? ?/sec    1.00     12.9±0.01µs        ? ?/sec
array_filter/utf8_view/keep50/null_rows50               1.13      9.5±0.01µs        ? ?/sec    1.00      8.4±0.01µs        ? ?/sec
array_filter/utf8_view/keep50/null_rows99               1.02      2.4±0.01µs        ? ?/sec    1.00      2.3±0.00µs        ? ?/sec
array_filter/utf8_view/keep90/no_nulls                  1.37     14.7±0.07µs        ? ?/sec    1.00     10.7±0.01µs        ? ?/sec
array_filter/utf8_view/keep_null_elements               1.68     10.1±0.07µs        ? ?/sec    1.00      6.0±0.01µs        ? ?/sec

Resource Usage

array_filter — base (merge-base)

Metric Value
Wall time 385.1s
Peak memory 60.7 MiB
Avg memory 30.3 MiB
CPU user 383.8s
CPU sys 0.3s
Peak spill 0 B

array_filter — branch

Metric Value
Wall time 410.1s
Peak memory 59.6 MiB
Avg memory 30.8 MiB
CPU user 391.0s
CPU sys 0.2s
Peak spill 0 B

File an issue against this benchmark runner

@comphead comphead left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I traced prepare_list_filter by reading and found no correctness problem. I checked the word-boundary handling in SetBitCounter, the lazy keep_mask copy and hidden_count across consecutive NULL rows. I did not run the tests or benches. The comments below are about test coverage and simplification.

  • array_compact.rs computes its per-row kept counts with count_set_bits_offset on every valid row. You measured that call as slow on short rows, so SetBitCounter would likely help there too. Fine as a follow-up.
  • In bench_case, selected and result_lengths start as Vec::new() although both sizes are known (values.len() and list.len()). Setup only.

Comment thread datafusion/functions-nested/src/utils.rs
Comment thread datafusion/functions-nested/src/utils.rs Outdated
Comment thread datafusion/functions-nested/src/lambda_utils.rs
Comment thread datafusion/functions-nested/src/array_filter.rs
Comment thread datafusion/functions-nested/src/utils.rs Outdated
@neilconway
neilconway disabled auto-merge October 15, 2026 16:18
When a list has no NULL rows, every set bit in the keep bitmap is kept, so
`prepare_list_filter` now builds each row's offset directly from the running
count of set bits and returns the bitmap as is. Lists with NULL rows use the
existing loop. Each branch creates its own `SetBitCounter`; sharing one made
int32/keep50/null_rows50 7% slower on an M4 Max because of how the compiler
assigned registers in the NULL-row loop.

`OffsetBuffer::new_unchecked` stays: the checked constructor made
int32/narrow_2/null_elements25 8% slower.

`adjusted_row_offsets` now copies the offsets of a sliced list once instead
of twice.

Also test `prepare_list_filter` against a reference implementation on seeded
random lists (i32 and i64 offsets, NULL rows that store elements, sliced
lists, random keep bitmaps), add an array_filter test where only values
stored under a NULL row pass the predicate, and size the benchmark's
reference vectors up front.

Benchmarks vs 021c8fd (M4 Max):

  - int32/narrow_2/null_elements25, 6.88 -> 4.11 us, -40%
  - int32/keep_all, 1.60 -> 1.22 us, -24%
  - int32/keep10/no_nulls, 1.90 -> 1.52 us, -20%
  - int32/keep50/null_elements25, 3.44 -> 3.06 us, -11%
  - int32/large_1m/null_elements25, 865 -> 781 us, -10%
  - cases with NULL rows, -2.5% to -0.2%
@neilconway
neilconway enabled auto-merge October 16, 2026 13:24
@neilconway

Copy link
Copy Markdown
Contributor Author

@comphead Thanks for the review!

I fixed the bench capacity nit you mentioned. Good point about possibly using SetBitCounter in array_compact; I'll take a look at that as a possible followup PR.

@neilconway
neilconway added this pull request to the merge queue Oct 16, 2026
Merged via the queue into apache:main with commit 8edd071 Oct 16, 2026
43 of 44 checks passed
@neilconway
neilconway deleted the neilc/refactor-array-filter branch October 16, 2026 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

functions Changes to functions implementation sqllogictest SQL Logic Tests (.slt) v56.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants