Skip to content

perf: Optimize array_has() for array needle#23337

Open
freakyzoidberg wants to merge 2 commits into
apache:mainfrom
freakyzoidberg:perf/array-has-array
Open

perf: Optimize array_has() for array needle#23337
freakyzoidberg wants to merge 2 commits into
apache:mainfrom
freakyzoidberg:perf/array-has-array

Conversation

@freakyzoidberg

@freakyzoidberg freakyzoidberg commented Jul 6, 2026

Copy link
Copy Markdown
Member

Which issue does this PR close?

The numbers below come from the committed criterion benchmark added in #23335 (cargo bench --bench array_has) — origin = the per-row eq kernel (unoptimized main / #23335), now = with this optimization applied. Run the bench on main and on this branch to reproduce.

Full disclosure - this was heavily assisted by AI, and I did my best to understand and justify every change here before submitting.

Rationale for this change

array_has(array, element) returns, for each row, whether the array contains the element.

When the element (needle) is an array rather than a scalar, the needle argument is a column with one value per row, e.g. array_has(t1.tags, t2.key) in a join filter, execution goes through array_has_dispatch_for_array (the ColumnarValue::Array needle branch), which compared each row by invoking the Arrow eq kernel once per row.

That kernel allocates a BooleanArray and pays downcast and dispatch overhead on every row. (The scalar-needle branch was optimized separately in #20374.)

What this removes is the fixed per-row kernel overhead, not the element comparison itself, so the gain is largest for short lists and shrinks as lists grow.

All numbers below are from the committed criterion benchmark (cargo bench --bench array_has, groups array_has_array_null_patterns / array_has_array_by_size / array_has_array_by_rows): the array_has UDF evaluated in isolation with an array needle, origin (the per-row eq kernel) vs now. "list length" is the number of elements in each row's array (not the row count). Not end-to-end query time.

By data type and null pattern (list length 64, 10K rows)

element element len null pattern origin now speedup
i64 - no nulls, found 1.10 ms 73 µs 15.1x
i64 - no nulls, not found 1.07 ms 72 µs. 14.9x
i64 - 30% nulls, found 1.17 ms 315 µs 3.7x
i64 - 30% nulls, not found 1.10 ms 274 µs 4.0x
i64 - all null 1.10 ms 272 µs 4.0x
i64 - collision 1.10 ms 270 µs 4.1x
Utf8 short (inline) no nulls 2.57 ms 1.01 ms 2.5x
Utf8 short (inline) 30% nulls 3.37 ms 1.52 ms 2.2x
Utf8 long (>12B) no nulls 2.61 ms 1.04 ms 2.5x
Utf8 long (>12B) 30% nulls 3.31 ms 1.52 ms 2.2x
Utf8 - all null 1.26 ms 256 µs 4.9x
LargeUtf8 short (inline) no nulls 2.56 ms 1.02 ms 2.5x
LargeUtf8 short (inline) 30% nulls 3.20 ms 1.54 ms 2.1x
LargeUtf8 long (>12B) no nulls 2.67 ms 1.05 ms 2.6x
LargeUtf8 long (>12B) 30% nulls 3.42 ms 1.59 ms 2.2x
LargeUtf8 - all null 1.31 ms 263 µs 5.0x
Utf8View short (inline) no nulls 1.18 ms 239 µs 4.9x
Utf8View short (inline) 30% nulls 1.26 ms 246 µs 5.1x
Utf8View long (>12B) no nulls 2.86 ms 1.17 ms 2.4x
Utf8View long (>12B) 30% nulls 3.51 ms 1.66 ms 2.1x
Utf8View - all null 1.20 ms 267 µs 4.5x

The i64 null cases are uniform (~4x) whether the match is present, absent, the whole list is null, or the needle collides with a null slot's backing fill value — validity is folded in with one word-parallel op, so there is no per-row rescan and no null slot can match.

Strings win ~2.1–2.5x mainly by dropping the per-row BooleanArray allocation. Utf8View additionally uses a view-aware compare: the byte length and 4-byte prefix packed into the 128-bit view reject non-matches before touching the data buffer, and an inline value (≤ 12 bytes) is matched by whole-view equality with no materialization at all — hence ~5x on short/inline strings. When long strings share a prefix (e.g. ARNs) the prefix can't reject, so Utf8View falls in line with the other string types (~2.1–2.4x). No string case regresses.

By list length (i64, 30% element nulls, not found, 10K rows)

elems/row origin now speedup
8 1.03 ms 111 µs 9.3x
32 1.07 ms 197 µs 5.5x
128 1.18 ms 446 µs 2.6x
256 1.28 ms 780 µs 1.6x
512 1.54 ms 1.44 ms 1.1x
1024 2.17 ms 2.15 ms 1.0x (falls back to per-row kernel)

The element-null branch makes a few passes over the values; past a moderate average list length (NULL_FAST_PATH_MAX_LEN) the per-row kernel wins, so it bails to it there — no meaningful regression. That average is measured over the visible (sliced) region, so a sliced array's hidden child elements can't route a small window to the slow path. The all-valid fold has no such crossover.

By row count (i64, 8 elems/row, 30% nulls, not found)

rows origin now speedup
10K 1.04 ms 111 µs 9.4x
100K 10.42 ms 1.09 ms 9.6x
1M 102.68 ms 10.91 ms 9.4x

Invariant to the number of rows — the per-row overhead removed is a fixed cost, so absolute savings scale linearly with the column height.

The remaining benchmarks in the suite (scalar array_has, array_has_all, array_has_any — paths this PR does not touch) are unchanged (median 0.99x, within measurement noise), confirming no regression outside the array-needle path.

End-to-end (context)

For a query dominated by an array-needle array_has join filter (a NestedLoopJoinExec with filter=array_has(tags, key) over 3000x3000 rows of 8-element lists) total time drops from 0.95s to 0.059s (~16x, identical results). For a workload where array_has is a smaller fraction, e.g. the ~6% of profile that motivated this (see #18070 / #18161, which fixed the join's deep-copy but left the per-row array_has cost), the overall speedup is single-digit percent.

What changes are included in this PR?

A fast path for primitive and string element types in array_has_dispatch_for_array, preserving the Arrow eq kernel semantics (total-order float equality; null elements never match):

  • All-valid elements: each row is a single branchless OR-reduction over the raw native value slice (auto-vectorizes; the common case).
  • Element nulls: a null slot's backing value is arbitrary, so the per-element equality bitmap is ANDed with the validity bitmap (one word-parallel op, no per-element branch) before reducing each row to "any bit set", a null slot can never match regardless of its value. This branch is processed in row chunks so the scratch buffer stays bounded, and past NULL_FAST_PATH_MAX_LEN average elements/row a length check over the visible (sliced) region bails to the per-row kernel (see the list-length table).
  • String elements: each row is a single pass over the row's values (compare, then consult validity only on a match). Utf8View compares the packed 128-bit views directly — length + 4-byte prefix reject non-matches before any data-buffer access, and an inline value (≤ 12 bytes) matches by whole-view equality with no materialization.
  • Nested (and any other) element types keep using the per-row eq kernel.

The array-needle benchmarks used for the numbers above are added in #3 (null patterns, list length, and row count).

Are these changes tested?

Yes:

  • New unit tests for the array-needle path covering element nulls, the null-fill collision (needle equal to a null slot's backing value), total-order float equality (NaN / -0.0), sliced arrays (including a small visible window over a large backing child), LargeList offsets, empty rows, a multi-chunk input, and a long-list input that exercises the per-row fallback, each cross-checked against the original per-row eq kernel as an oracle.
  • Existing array_has / array_contains / join_lists sqllogictest suites pass.

Are there any user-facing changes?

No.

@gabotechs

Copy link
Copy Markdown
Contributor

run benchmarks array_has

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c4893049695-863-wwkw9 6.12.85+ #1 SMP Mon May 11 08:17:35 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 perf/array-has-array (ab0504c) to a0e9887 (merge-base) diff using: array_has
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)

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                                    perf_array-has-array
-----                                          ----                                    --------------------
array_has_all/all_found_small_needle/10        1.00    740.8±0.90µs        ? ?/sec     1.00    739.9±2.01µs        ? ?/sec
array_has_all/all_found_small_needle/100       1.00      4.9±0.02ms        ? ?/sec     1.00      4.9±0.02ms        ? ?/sec
array_has_all/all_found_small_needle/500       1.00     23.4±0.21ms        ? ?/sec     1.00     23.4±0.18ms        ? ?/sec
array_has_all/not_all_found/10                 1.00    523.3±0.60µs        ? ?/sec     1.00    525.7±1.41µs        ? ?/sec
array_has_all/not_all_found/100                1.01      4.3±0.03ms        ? ?/sec     1.00      4.2±0.03ms        ? ?/sec
array_has_all/not_all_found/500                1.01     20.6±0.08ms        ? ?/sec     1.00     20.5±0.04ms        ? ?/sec
array_has_all_strings/all_found/10             1.00   1094.0±1.39µs        ? ?/sec     1.01   1103.6±1.36µs        ? ?/sec
array_has_all_strings/all_found/100            1.01      6.5±0.01ms        ? ?/sec     1.00      6.4±0.01ms        ? ?/sec
array_has_all_strings/all_found/500            1.01     32.3±0.42ms        ? ?/sec     1.00     31.9±0.18ms        ? ?/sec
array_has_all_strings/not_all_found/10         1.00    753.9±1.07µs        ? ?/sec     1.01    761.7±1.44µs        ? ?/sec
array_has_all_strings/not_all_found/100        1.00      5.6±0.01ms        ? ?/sec     1.01      5.7±0.01ms        ? ?/sec
array_has_all_strings/not_all_found/500        1.00     34.0±0.14ms        ? ?/sec     1.01     34.3±0.05ms        ? ?/sec
array_has_any/no_match/10                      1.01    984.4±2.01µs        ? ?/sec     1.00    973.2±1.60µs        ? ?/sec
array_has_any/no_match/100                     1.00      8.4±0.02ms        ? ?/sec     1.00      8.4±0.03ms        ? ?/sec
array_has_any/no_match/500                     1.02     41.7±0.84ms        ? ?/sec     1.00     40.8±0.06ms        ? ?/sec
array_has_any/scalar_no_match/10               1.00    981.8±2.84µs        ? ?/sec     1.01    994.2±2.45µs        ? ?/sec
array_has_any/scalar_no_match/100              1.00      9.6±0.05ms        ? ?/sec     1.00      9.5±0.07ms        ? ?/sec
array_has_any/scalar_no_match/500              1.01     72.5±1.43ms        ? ?/sec     1.00     71.7±0.34ms        ? ?/sec
array_has_any/scalar_some_match/10             1.00    561.6±1.09µs        ? ?/sec     1.03    576.1±1.79µs        ? ?/sec
array_has_any/scalar_some_match/100            1.00      4.7±0.07ms        ? ?/sec     1.00      4.7±0.06ms        ? ?/sec
array_has_any/scalar_some_match/500            1.00     47.7±0.36ms        ? ?/sec     1.00     47.5±0.16ms        ? ?/sec
array_has_any/some_match/10                    1.02    660.7±0.94µs        ? ?/sec     1.00    646.1±1.19µs        ? ?/sec
array_has_any/some_match/100                   1.02      4.7±0.03ms        ? ?/sec     1.00      4.6±0.02ms        ? ?/sec
array_has_any/some_match/500                   1.00     22.5±0.05ms        ? ?/sec     1.00     22.4±0.02ms        ? ?/sec
array_has_any_scalar/i64_no_match/1            1.00    162.5±0.12µs        ? ?/sec     1.02    165.9±0.20µs        ? ?/sec
array_has_any_scalar/i64_no_match/10           1.00   211.6±10.40µs        ? ?/sec     1.01    213.7±5.71µs        ? ?/sec
array_has_any_scalar/i64_no_match/100          1.00   363.8±14.84µs        ? ?/sec     1.02   369.6±15.42µs        ? ?/sec
array_has_any_scalar/i64_no_match/1000         1.00    273.7±7.12µs        ? ?/sec     1.01    277.5±8.21µs        ? ?/sec
array_has_any_scalar/string_no_match/1         1.00    146.5±0.23µs        ? ?/sec     1.00    147.3±0.35µs        ? ?/sec
array_has_any_scalar/string_no_match/10        1.00    215.8±5.61µs        ? ?/sec     1.01    218.8±4.99µs        ? ?/sec
array_has_any_scalar/string_no_match/100       1.00   332.5±11.75µs        ? ?/sec     1.01   334.2±14.25µs        ? ?/sec
array_has_any_scalar/string_no_match/1000      1.00    241.2±4.79µs        ? ?/sec     1.01    244.3±5.00µs        ? ?/sec
array_has_any_strings/no_match/10              1.00   1280.6±1.56µs        ? ?/sec     1.04   1337.0±2.02µs        ? ?/sec
array_has_any_strings/no_match/100             1.00     10.0±0.01ms        ? ?/sec     1.06     10.5±0.01ms        ? ?/sec
array_has_any_strings/no_match/500             1.00     62.9±0.13ms        ? ?/sec     1.04     65.1±0.05ms        ? ?/sec
array_has_any_strings/scalar_no_match/10       1.00    467.3±1.03µs        ? ?/sec     1.01    473.4±0.72µs        ? ?/sec
array_has_any_strings/scalar_no_match/100      1.00      4.1±0.01ms        ? ?/sec     1.01      4.1±0.01ms        ? ?/sec
array_has_any_strings/scalar_no_match/500      1.08     45.6±0.07ms        ? ?/sec     1.00     42.0±0.12ms        ? ?/sec
array_has_any_strings/scalar_some_match/10     1.05    433.9±0.44µs        ? ?/sec     1.00    413.2±0.49µs        ? ?/sec
array_has_any_strings/scalar_some_match/100    1.01      2.0±0.01ms        ? ?/sec     1.00   1983.1±3.78µs        ? ?/sec
array_has_any_strings/scalar_some_match/500    1.08      8.7±0.17ms        ? ?/sec     1.00      8.1±0.14ms        ? ?/sec
array_has_any_strings/some_match/10            1.00    965.0±1.31µs        ? ?/sec     1.03    992.7±1.77µs        ? ?/sec
array_has_any_strings/some_match/100           1.00      5.9±0.06ms        ? ?/sec     1.04      6.2±0.03ms        ? ?/sec
array_has_any_strings/some_match/500           1.00     28.9±0.04ms        ? ?/sec     1.04     30.0±0.03ms        ? ?/sec
array_has_i64/found/10                         1.00     85.2±0.24µs        ? ?/sec     1.01     86.0±0.27µs        ? ?/sec
array_has_i64/found/100                        1.00   303.2±12.33µs        ? ?/sec     1.08   328.2±17.02µs        ? ?/sec
array_has_i64/found/500                        1.07  1677.3±95.87µs        ? ?/sec     1.00  1561.4±145.12µs        ? ?/sec
array_has_i64/not_found/10                     1.00     54.4±0.06µs        ? ?/sec     1.02     55.6±0.11µs        ? ?/sec
array_has_i64/not_found/100                    1.02   283.5±18.22µs        ? ?/sec     1.00   279.2±10.62µs        ? ?/sec
array_has_i64/not_found/500                    1.44  1848.4±119.39µs        ? ?/sec    1.00  1286.0±85.27µs        ? ?/sec
array_has_strings/found/10                     1.00    340.5±0.59µs        ? ?/sec     1.08    367.3±0.98µs        ? ?/sec
array_has_strings/found/100                    1.00  1699.0±39.31µs        ? ?/sec     1.00  1704.1±25.47µs        ? ?/sec
array_has_strings/found/500                    1.00      5.1±0.08ms        ? ?/sec     1.03      5.2±0.07ms        ? ?/sec
array_has_strings/not_found/10                 1.03    105.8±4.35µs        ? ?/sec     1.00    102.4±0.10µs        ? ?/sec
array_has_strings/not_found/100                1.00      3.1±0.01ms        ? ?/sec     1.03      3.2±0.01ms        ? ?/sec
array_has_strings/not_found/500                1.00     10.9±0.07ms        ? ?/sec     1.02     11.1±0.17ms        ? ?/sec

Resource Usage

array_has — base (merge-base)

Metric Value
Wall time 895.2s
Peak memory 158.8 MiB
Avg memory 40.2 MiB
CPU user 745.0s
CPU sys 10.6s
Peak spill 0 B

array_has — branch

Metric Value
Wall time 880.2s
Peak memory 155.7 MiB
Avg memory 38.4 MiB
CPU user 731.6s
CPU sys 11.8s
Peak spill 0 B

File an issue against this benchmark runner

@freakyzoidberg freakyzoidberg force-pushed the perf/array-has-array branch from ab0504c to 7ed6f01 Compare July 6, 2026 15:00
@github-actions github-actions Bot added the functions Changes to functions implementation label Jul 6, 2026
@gabotechs

Copy link
Copy Markdown
Contributor

run benchmarks array_has

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c4894297183-864-pdv6g 6.12.85+ #1 SMP Mon May 11 08:17:35 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 perf/array-has-array (7ed6f01) to 0365d3c (merge-base) diff using: array_has
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)

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                                   perf_array-has-array
-----                                                     ----                                   --------------------
array_has_all/all_found_small_needle/10                   1.00    737.1±0.95µs        ? ?/sec    1.00    737.1±0.74µs        ? ?/sec
array_has_all/all_found_small_needle/100                  1.01      4.9±0.01ms        ? ?/sec    1.00      4.9±0.00ms        ? ?/sec
array_has_all/all_found_small_needle/500                  1.00     23.3±0.02ms        ? ?/sec    1.00     23.3±0.02ms        ? ?/sec
array_has_all/not_all_found/10                            1.00    525.1±0.72µs        ? ?/sec    1.00    527.0±1.51µs        ? ?/sec
array_has_all/not_all_found/100                           1.00      4.2±0.01ms        ? ?/sec    1.00      4.2±0.00ms        ? ?/sec
array_has_all/not_all_found/500                           1.03     21.0±0.40ms        ? ?/sec    1.00     20.5±0.02ms        ? ?/sec
array_has_all_strings/all_found/10                        1.00   1094.8±0.92µs        ? ?/sec    1.00   1093.0±1.15µs        ? ?/sec
array_has_all_strings/all_found/100                       1.00      6.4±0.01ms        ? ?/sec    1.00      6.4±0.01ms        ? ?/sec
array_has_all_strings/all_found/500                       1.00     31.4±0.04ms        ? ?/sec    1.00     31.4±0.04ms        ? ?/sec
array_has_all_strings/not_all_found/10                    1.00    754.2±2.40µs        ? ?/sec    1.00    753.7±0.75µs        ? ?/sec
array_has_all_strings/not_all_found/100                   1.00      5.6±0.01ms        ? ?/sec    1.00      5.6±0.01ms        ? ?/sec
array_has_all_strings/not_all_found/500                   1.00     34.0±0.04ms        ? ?/sec    1.00     34.0±0.03ms        ? ?/sec
array_has_any/no_match/10                                 1.01    982.5±1.27µs        ? ?/sec    1.00    970.1±1.61µs        ? ?/sec
array_has_any/no_match/100                                1.01      8.5±0.02ms        ? ?/sec    1.00      8.4±0.00ms        ? ?/sec
array_has_any/no_match/500                                1.00     40.8±0.03ms        ? ?/sec    1.00     40.8±0.03ms        ? ?/sec
array_has_any/scalar_no_match/10                          1.00    851.2±2.39µs        ? ?/sec    1.16    985.6±0.56µs        ? ?/sec
array_has_any/scalar_no_match/100                         1.00      8.2±0.03ms        ? ?/sec    1.16      9.6±0.01ms        ? ?/sec
array_has_any/scalar_no_match/500                         1.00     63.3±0.40ms        ? ?/sec    1.13     71.4±0.16ms        ? ?/sec
array_has_any/scalar_some_match/10                        1.00    540.1±4.43µs        ? ?/sec    1.06    571.0±1.83µs        ? ?/sec
array_has_any/scalar_some_match/100                       1.00      4.5±0.04ms        ? ?/sec    1.06      4.8±0.01ms        ? ?/sec
array_has_any/scalar_some_match/500                       1.00     43.4±0.13ms        ? ?/sec    1.08     46.8±0.09ms        ? ?/sec
array_has_any/some_match/10                               1.01    658.6±0.69µs        ? ?/sec    1.00    649.1±0.85µs        ? ?/sec
array_has_any/some_match/100                              1.01      4.7±0.01ms        ? ?/sec    1.00      4.6±0.00ms        ? ?/sec
array_has_any/some_match/500                              1.01     22.6±0.02ms        ? ?/sec    1.00     22.4±0.02ms        ? ?/sec
array_has_any_scalar/i64_no_match/1                       1.00    159.1±0.51µs        ? ?/sec    1.05    166.8±2.34µs        ? ?/sec
array_has_any_scalar/i64_no_match/10                      1.02    212.2±5.37µs        ? ?/sec    1.00    208.5±6.85µs        ? ?/sec
array_has_any_scalar/i64_no_match/100                     1.01   370.1±16.92µs        ? ?/sec    1.00   367.3±15.67µs        ? ?/sec
array_has_any_scalar/i64_no_match/1000                    1.02    278.8±6.60µs        ? ?/sec    1.00    274.2±7.29µs        ? ?/sec
array_has_any_scalar/string_no_match/1                    1.00    147.0±0.20µs        ? ?/sec    1.04    152.7±0.27µs        ? ?/sec
array_has_any_scalar/string_no_match/10                   1.00    214.4±5.22µs        ? ?/sec    1.00    214.1±5.53µs        ? ?/sec
array_has_any_scalar/string_no_match/100                  1.01   334.8±12.07µs        ? ?/sec    1.00   330.9±13.28µs        ? ?/sec
array_has_any_scalar/string_no_match/1000                 1.00    242.4±3.95µs        ? ?/sec    1.00    241.4±5.34µs        ? ?/sec
array_has_any_strings/no_match/10                         1.00   1275.3±1.67µs        ? ?/sec    1.01   1289.1±1.49µs        ? ?/sec
array_has_any_strings/no_match/100                        1.00      9.9±0.01ms        ? ?/sec    1.00      9.9±0.01ms        ? ?/sec
array_has_any_strings/no_match/500                        1.00     62.5±0.04ms        ? ?/sec    1.00     62.8±0.05ms        ? ?/sec
array_has_any_strings/scalar_no_match/10                  1.00    476.8±0.80µs        ? ?/sec    1.03    493.1±1.61µs        ? ?/sec
array_has_any_strings/scalar_no_match/100                 1.00      4.2±0.01ms        ? ?/sec    1.05      4.3±0.00ms        ? ?/sec
array_has_any_strings/scalar_no_match/500                 1.00     41.5±0.10ms        ? ?/sec    1.04     43.1±0.11ms        ? ?/sec
array_has_any_strings/scalar_some_match/10                1.00    412.4±0.54µs        ? ?/sec    1.02    418.7±0.59µs        ? ?/sec
array_has_any_strings/scalar_some_match/100               1.00   1994.7±4.42µs        ? ?/sec    1.02      2.0±0.00ms        ? ?/sec
array_has_any_strings/scalar_some_match/500               1.00      8.3±0.11ms        ? ?/sec    1.01      8.4±0.08ms        ? ?/sec
array_has_any_strings/some_match/10                       1.00    964.1±1.14µs        ? ?/sec    1.00    965.6±1.64µs        ? ?/sec
array_has_any_strings/some_match/100                      1.00      5.8±0.01ms        ? ?/sec    1.00      5.8±0.01ms        ? ?/sec
array_has_any_strings/some_match/500                      1.00     28.8±0.03ms        ? ?/sec    1.00     28.8±0.03ms        ? ?/sec
array_has_array_by_rows/10000                             10.15     3.1±0.04ms        ? ?/sec    1.00    304.5±7.16µs        ? ?/sec
array_has_array_by_rows/100000                            9.80     30.7±0.43ms        ? ?/sec    1.00      3.1±0.07ms        ? ?/sec
array_has_array_by_rows/1000000                           9.14    299.8±1.32ms        ? ?/sec    1.00     32.8±0.71ms        ? ?/sec
array_has_array_by_size/1024                              1.03      8.2±0.12ms        ? ?/sec    1.00      8.0±0.11ms        ? ?/sec
array_has_array_by_size/128                               3.61      3.3±0.03ms        ? ?/sec    1.00   901.7±12.07µs        ? ?/sec
array_has_array_by_size/256                               2.30      3.6±0.07ms        ? ?/sec    1.00  1577.5±25.59µs        ? ?/sec
array_has_array_by_size/32                                7.35      3.1±0.01ms        ? ?/sec    1.00    417.9±7.21µs        ? ?/sec
array_has_array_by_size/512                               1.79      5.7±0.06ms        ? ?/sec    1.00      3.2±0.04ms        ? ?/sec
array_has_array_by_size/8                                 9.96      3.0±0.01ms        ? ?/sec    1.00    304.5±7.12µs        ? ?/sec
array_has_array_i64/found/10                              8.97      3.1±0.02ms        ? ?/sec    1.00    344.3±6.86µs        ? ?/sec
array_has_array_i64/found/100                             4.28      3.3±0.02ms        ? ?/sec    1.00    781.1±7.81µs        ? ?/sec
array_has_array_i64/found/500                             1.77      5.5±0.06ms        ? ?/sec    1.00      3.1±0.03ms        ? ?/sec
array_has_array_i64/not_found/10                          9.91      3.1±0.01ms        ? ?/sec    1.00    310.4±7.13µs        ? ?/sec
array_has_array_i64/not_found/100                         4.26      3.3±0.03ms        ? ?/sec    1.00   779.2±10.38µs        ? ?/sec
array_has_array_i64/not_found/500                         1.77      5.5±0.06ms        ? ?/sec    1.00      3.1±0.04ms        ? ?/sec
array_has_array_null_patterns/i64/all_null                5.72      3.1±0.05ms        ? ?/sec    1.00   541.8±11.15µs        ? ?/sec
array_has_array_null_patterns/i64/collision               5.64      3.1±0.02ms        ? ?/sec    1.00   540.3±12.21µs        ? ?/sec
array_has_array_null_patterns/i64/no_nulls                20.82     2.6±0.03ms        ? ?/sec    1.00    126.4±1.42µs        ? ?/sec
array_has_array_null_patterns/i64/no_nulls_not_found      20.58     2.6±0.02ms        ? ?/sec    1.00    126.4±1.85µs        ? ?/sec
array_has_array_null_patterns/i64/nulls30_found           5.75      3.2±0.05ms        ? ?/sec    1.00    552.1±7.01µs        ? ?/sec
array_has_array_null_patterns/i64/nulls30_not_found       5.58      3.0±0.02ms        ? ?/sec    1.00   546.8±13.34µs        ? ?/sec
array_has_array_null_patterns/largeutf8/all_null          7.53      3.5±0.03ms        ? ?/sec    1.00    459.1±8.77µs        ? ?/sec
array_has_array_null_patterns/largeutf8_long/no_nulls     2.83      4.9±0.04ms        ? ?/sec    1.00  1740.1±15.23µs        ? ?/sec
array_has_array_null_patterns/largeutf8_long/nulls30      2.45      5.8±0.05ms        ? ?/sec    1.00      2.4±0.01ms        ? ?/sec
array_has_array_null_patterns/largeutf8_short/no_nulls    3.17      4.5±0.05ms        ? ?/sec    1.00   1431.8±5.68µs        ? ?/sec
array_has_array_null_patterns/largeutf8_short/nulls30     2.57      5.5±0.04ms        ? ?/sec    1.00      2.1±0.01ms        ? ?/sec
array_has_array_null_patterns/utf8/all_null               7.20      3.5±0.03ms        ? ?/sec    1.00    481.3±1.35µs        ? ?/sec
array_has_array_null_patterns/utf8_long/no_nulls          2.96      5.0±0.05ms        ? ?/sec    1.00  1690.8±18.28µs        ? ?/sec
array_has_array_null_patterns/utf8_long/nulls30           2.52      5.9±0.03ms        ? ?/sec    1.00      2.3±0.01ms        ? ?/sec
array_has_array_null_patterns/utf8_short/no_nulls         3.29      4.6±0.02ms        ? ?/sec    1.00   1394.3±3.14µs        ? ?/sec
array_has_array_null_patterns/utf8_short/nulls30          2.71      5.7±0.04ms        ? ?/sec    1.00      2.1±0.01ms        ? ?/sec
array_has_array_null_patterns/utf8view/all_null           7.27      3.6±0.08ms        ? ?/sec    1.00    493.2±5.26µs        ? ?/sec
array_has_array_null_patterns/utf8view_long/no_nulls      2.82      5.8±0.08ms        ? ?/sec    1.00      2.1±0.02ms        ? ?/sec
array_has_array_null_patterns/utf8view_long/nulls30       2.53      6.4±0.07ms        ? ?/sec    1.00      2.5±0.01ms        ? ?/sec
array_has_array_null_patterns/utf8view_short/no_nulls     6.69      3.1±0.08ms        ? ?/sec    1.00    464.0±3.46µs        ? ?/sec
array_has_array_null_patterns/utf8view_short/nulls30      7.24      3.4±0.07ms        ? ?/sec    1.00    473.7±3.37µs        ? ?/sec
array_has_array_strings/found/10                          11.03     3.5±0.03ms        ? ?/sec    1.00    317.1±1.04µs        ? ?/sec
array_has_array_strings/found/100                         2.77      6.3±0.01ms        ? ?/sec    1.00      2.3±0.00ms        ? ?/sec
array_has_array_strings/found/500                         1.60     21.2±0.10ms        ? ?/sec    1.00     13.2±0.12ms        ? ?/sec
array_has_array_strings/not_found/10                      26.98     3.5±0.01ms        ? ?/sec    1.00    129.3±0.08µs        ? ?/sec
array_has_array_strings/not_found/100                     5.26      3.8±0.01ms        ? ?/sec    1.00    724.5±1.09µs        ? ?/sec
array_has_array_strings/not_found/500                     1.88      7.2±0.04ms        ? ?/sec    1.00      3.8±0.01ms        ? ?/sec
array_has_i64/found/10                                    1.01     85.2±0.26µs        ? ?/sec    1.00     84.6±0.14µs        ? ?/sec
array_has_i64/found/100                                   1.00    274.8±2.48µs        ? ?/sec    1.05    287.5±2.59µs        ? ?/sec
array_has_i64/found/500                                   1.00  1465.3±45.63µs        ? ?/sec    1.07  1562.6±29.17µs        ? ?/sec
array_has_i64/not_found/10                                1.00     54.4±0.06µs        ? ?/sec    1.00     54.4±0.07µs        ? ?/sec
array_has_i64/not_found/100                               1.00    227.3±2.77µs        ? ?/sec    1.04    236.2±2.77µs        ? ?/sec
array_has_i64/not_found/500                               1.00  1448.8±29.15µs        ? ?/sec    1.01  1468.4±26.19µs        ? ?/sec
array_has_strings/found/10                                1.10    368.3±0.59µs        ? ?/sec    1.00    336.1±0.60µs        ? ?/sec
array_has_strings/found/100                               1.01   1616.1±2.88µs        ? ?/sec    1.00   1604.8±1.67µs        ? ?/sec
array_has_strings/found/500                               1.04      5.0±0.05ms        ? ?/sec    1.00      4.8±0.02ms        ? ?/sec
array_has_strings/not_found/10                            1.00    102.1±0.10µs        ? ?/sec    1.00    102.1±0.14µs        ? ?/sec
array_has_strings/not_found/100                           1.04      3.3±0.01ms        ? ?/sec    1.00      3.1±0.01ms        ? ?/sec
array_has_strings/not_found/500                           1.01     11.1±0.05ms        ? ?/sec    1.00     11.0±0.05ms        ? ?/sec

Resource Usage

array_has — base (merge-base)

Metric Value
Wall time 1235.3s
Peak memory 155.5 MiB
Avg memory 48.7 MiB
CPU user 1255.5s
CPU sys 11.3s
Peak spill 0 B

array_has — branch

Metric Value
Wall time 1260.3s
Peak memory 155.7 MiB
Avg memory 47.3 MiB
CPU user 1289.9s
CPU sys 12.1s
Peak spill 0 B

File an issue against this benchmark runner

@gabotechs

Copy link
Copy Markdown
Contributor

Those are some pretty good results!

`array_has(array, element)` returns, for each row, whether the array contains
the element. When the `element` (needle) is an array rather than a scalar -- the
needle argument is a column with one value per row, e.g. `array_has(t1.tags,
t2.key)` in a join filter -- execution goes through `array_has_dispatch_for_array`
(the `ColumnarValue::Array` needle branch), which compared each row by invoking
the Arrow `eq` kernel once per row. That kernel allocates a `BooleanArray` and
pays downcast and dispatch overhead on every row. (The scalar-needle branch was
optimized separately in apache#20374.)

Add a fast path for primitive and string element types, preserving the Arrow
`eq` kernel semantics (total-order float equality; null elements never match).
With all-valid elements each row is a single branchless OR-reduction over the
native values. When primitive elements contain nulls -- whose backing values
are arbitrary -- the per-element equality bitmap is ANDed with the validity
bitmap (one word-parallel op, no per-element branch) before the per-row
reduction, so null slots never match regardless of their value. Past a moderate
average list length (`NULL_FAST_PATH_MAX_LEN`) this bitmap's extra passes lose to
the per-row kernel, so the element-null branch bails to it there; that length is
measured over the visible (sliced) region, so a sliced array's hidden child
elements don't route a small window to the slow path. The all-valid fold has no
such crossover.

For string elements each row is a single pass over the row's values; for
`Utf8View` this is view-aware -- the byte length and 4-byte prefix packed in the
128-bit view reject non-matches before touching the data buffer (what the `eq`
kernel does, but without its per-row allocation), and an inline needle (<= 12
bytes) is matched by full-view equality with no materialization at all. Nested
and other element types keep using the per-row `eq` kernel.

What this removes is the fixed per-row kernel overhead, not the element
comparison itself, so the gain is largest for short lists and shrinks as lists
grow. Criterion microbenchmark, array needle on a 100k-row batch vs the per-row
`eq` kernel (NOT end-to-end query time):

    i64, all-valid elements:  ~15x (64-elem lists), ~1.9x at 1024-elem
    i64, ~30% null elements:  ~9x (8-elem) ... ~1x (512-elem); falls back beyond
    Utf8 / LargeUtf8:          ~2.5x
    Utf8View (view-aware):     ~5x on short/inline strings

Every shape improves with no regression.

End-to-end impact depends on how much of a query `array_has` accounts for. For a
query dominated by an array-needle `array_has` join filter (a `NestedLoopJoinExec`
with `filter=array_has(tags, key)` over 3000x3000 rows of 8-element lists) total
time drops from 0.95s to 0.059s (~16x, identical results). For a workload where
`array_has` is a smaller fraction -- e.g. the ~6% of profile that motivated this
(see apache#18070 / apache#18161, which fixed the join's deep-copy but left the per-row
`array_has` cost) -- the overall speedup is single-digit percent.

The array-needle criterion benchmark used for these numbers is added separately
(so it can be run against `main` to capture the before baseline).

Part of apache#18727.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@freakyzoidberg freakyzoidberg force-pushed the perf/array-has-array branch from 7ed6f01 to 7598605 Compare July 6, 2026 19:36
Move the behavioral coverage for the array (column) needle path from Rust unit
tests into datafusion/sqllogictest/test_files/array/array_has.slt (element and
needle nulls, null/empty rows, the null-fill collision, found/not-found, over
i64, Utf8, Utf8View, LargeList and LargeUtf8, plus a >512-row case that
exercises the chunked element-null path), following the maintainer preference
for covering UDFs with SLT.

Keep a single unit test for sliced List / FixedSizeList offset handling, whose
sliced-array shape SQL can't produce. Drops the reference oracle and the
string-list test helper, no longer needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the sqllogictest SQL Logic Tests (.slt) label Jul 6, 2026
@freakyzoidberg freakyzoidberg marked this pull request as ready for review July 6, 2026 21:28
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)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants