Skip to content

perf: optimize array_remove for scalar needle#22390

Merged
Jefffrey merged 10 commits into
apache:mainfrom
lyne7-sc:perf/remove
May 25, 2026
Merged

perf: optimize array_remove for scalar needle#22390
Jefffrey merged 10 commits into
apache:mainfrom
lyne7-sc:perf/remove

Conversation

@lyne7-sc
Copy link
Copy Markdown
Contributor

@lyne7-sc lyne7-sc commented May 20, 2026

Which issue does this PR close?

  • Closes #.

Rationale for this change

Similar to #22387 (array_replace scalar optimization)

array_remove / array_remove_n / array_remove_all perform element-wise comparison by invoking compare_element_to_list against each row's sub-array individually. When the needle is a scalar, this can be optimized by performing a single vectorized distinct comparison over the entire flattened values buffer.

What changes are included in this PR?

  • Add a specialized removal kernel (general_remove_with_scalar) that uses arrow_ord::cmp::distinct with Scalar wrapper for a single bulk comparison pass over the flat values buffer.
  • Extend SLT tests with multi-row scalar-argument coverage, NULL-containing arrays, empty-array edge cases, boundary n values, and LargeList type coverage.

Benchmarks

group                                                                    main                                   optimized
-----                                                                    ----                                   ---------
array_remove_all_int64/remove/list size: 10, num_rows: 4000              4.35   856.8±97.81µs        ? ?/sec    1.00    196.9±4.48µs        ? ?/sec
array_remove_all_int64/remove/list size: 100, num_rows: 10000            1.90      5.5±0.09ms        ? ?/sec    1.00      2.9±0.09ms        ? ?/sec
array_remove_all_int64/remove/list size: 500, num_rows: 10000            1.35     19.2±0.21ms        ? ?/sec    1.00     14.2±0.48ms        ? ?/sec
array_remove_all_int64_nested/remove/list size: 10, num_rows: 4000       1.00      7.1±0.12ms        ? ?/sec    1.04      7.4±0.12ms        ? ?/sec
array_remove_all_int64_nested/remove/list size: 100, num_rows: 3000      1.00     36.5±0.39ms        ? ?/sec    1.05     38.3±2.61ms        ? ?/sec
array_remove_all_int64_nested/remove/list size: 300, num_rows: 1500      1.01     53.5±2.26ms        ? ?/sec    1.00     53.0±0.99ms        ? ?/sec
array_remove_boolean/remove/list size: 10, num_rows: 4000                3.83    813.9±7.08µs        ? ?/sec    1.00    212.4±2.28µs        ? ?/sec
array_remove_boolean/remove/list size: 100, num_rows: 10000              2.73      3.7±0.03ms        ? ?/sec    1.00  1364.7±177.83µs        ? ?/sec
array_remove_boolean/remove/list size: 500, num_rows: 10000              2.34      9.8±0.14ms        ? ?/sec    1.00      4.2±0.25ms        ? ?/sec
array_remove_fixed_size_binary/remove/list size: 10, num_rows: 4000      3.16   918.2±16.76µs        ? ?/sec    1.00    290.6±9.79µs        ? ?/sec
array_remove_fixed_size_binary/remove/list size: 100, num_rows: 10000    1.56      6.9±0.13ms        ? ?/sec    1.00      4.4±0.15ms        ? ?/sec
array_remove_fixed_size_binary/remove/list size: 500, num_rows: 10000    1.17     27.7±0.84ms        ? ?/sec    1.00     23.6±2.04ms        ? ?/sec
array_remove_int64/remove/list size: 10, num_rows: 4000                  4.55    825.7±6.30µs        ? ?/sec    1.00    181.3±4.32µs        ? ?/sec
array_remove_int64/remove/list size: 100, num_rows: 10000                3.35      3.8±0.11ms        ? ?/sec    1.00  1135.6±54.87µs        ? ?/sec
array_remove_int64/remove/list size: 500, num_rows: 10000                2.04     10.3±0.35ms        ? ?/sec    1.00      5.1±0.39ms        ? ?/sec
array_remove_int64_nested/remove/list size: 10, num_rows: 4000           1.00      7.1±0.18ms        ? ?/sec    1.02      7.2±0.07ms        ? ?/sec
array_remove_int64_nested/remove/list size: 100, num_rows: 3000          1.00     36.1±1.35ms        ? ?/sec    1.07     38.5±3.67ms        ? ?/sec
array_remove_int64_nested/remove/list size: 300, num_rows: 1500          1.00     51.7±0.57ms        ? ?/sec    1.05     54.1±2.13ms        ? ?/sec
array_remove_n_int64/remove/list size: 10, num_rows: 4000                4.43    845.3±5.00µs        ? ?/sec    1.00    190.6±2.84µs        ? ?/sec
array_remove_n_int64/remove/list size: 100, num_rows: 10000              2.29      4.7±0.11ms        ? ?/sec    1.00      2.0±0.12ms        ? ?/sec
array_remove_n_int64/remove/list size: 500, num_rows: 10000              1.63     14.8±0.42ms        ? ?/sec    1.00      9.0±0.51ms        ? ?/sec
array_remove_n_int64_nested/remove/list size: 10, num_rows: 4000         1.00      7.0±0.09ms        ? ?/sec    1.29      8.9±3.44ms        ? ?/sec
array_remove_n_int64_nested/remove/list size: 100, num_rows: 3000        1.00     36.6±0.42ms        ? ?/sec    1.03     37.7±0.68ms        ? ?/sec
array_remove_n_int64_nested/remove/list size: 300, num_rows: 1500        1.00     52.7±3.68ms        ? ?/sec    1.03     54.5±4.49ms        ? ?/sec
array_remove_strings/remove/list size: 10, num_rows: 4000                2.50  1144.6±21.95µs        ? ?/sec    1.00   457.0±14.15µs        ? ?/sec
array_remove_strings/remove/list size: 100, num_rows: 10000              1.42     10.5±1.16ms        ? ?/sec    1.00      7.4±0.34ms        ? ?/sec
array_remove_strings/remove/list size: 500, num_rows: 10000              1.12     39.8±0.91ms        ? ?/sec    1.00     35.5±1.51ms        ? ?/sec

Are these changes tested?

Yes, existing and new SLT edge-case tests in array_remove.slt.

Are there any user-facing changes?

No.

@github-actions github-actions Bot added sqllogictest SQL Logic Tests (.slt) functions Changes to functions implementation labels May 20, 2026
Copy link
Copy Markdown
Contributor

@neilconway neilconway left a comment

Choose a reason for hiding this comment

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

Nice performance win!

);
}
};
let original_data = list_array.values().to_data();
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.

This will be inefficient for sliced arrays.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I now slice the values to the range actually referenced by the offsets.

That said, I wanted to understand your concern better: when a GenericListArray is sliced, values() returns the full underlying array, and to_data() on it wraps the existing buffer references into ArrayData without copying. So the main downside I could identify is that Capacities::Array(original_data.len()) over-estimates the pre-allocation for sliced inputs. Were you thinking of a different inefficiency, or is the over-allocation what you had in mind?

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.

The over-allocation was one part, but the bigger concern is calling the distinct kernel on the entire values buffer (see other comment).

Comment thread datafusion/functions-nested/src/remove.rs Outdated
Comment thread datafusion/functions-nested/src/remove.rs Outdated
Comment thread datafusion/functions-nested/src/remove.rs Outdated
Comment on lines +607 to +608
// Iterate only over the positions that need removal using set_indices,
// which is more efficient than scanning every bit.
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.

Might be worth elaborating that the win here is mostly because we expect the # of values-to-remove is a lot smaller than the total array size, which it usually (but not always) will be.

Comment on lines +571 to +572
let keep_mask =
arrow_ord::cmp::distinct(list_array.values(), &Scalar::new(Arc::clone(needle)))?;
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.

This will call the distinct kernel on all the elements in the value buffer, not just the ones that are visible in a sliced array.

);
}
};
let original_data = list_array.values().to_data();
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.

The over-allocation was one part, but the bigger concern is calling the distinct kernel on the entire values buffer (see other comment).

Copy link
Copy Markdown
Contributor

@neilconway neilconway left a comment

Choose a reason for hiding this comment

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

Awesome, nice work on this! lgtm. Maybe update the benchmark numbers in the PR description if you get a chance.

@lyne7-sc
Copy link
Copy Markdown
Contributor Author

Thanks @neilconway! Really appreciate the thorough and speedy review 🙏 Great suggestion — running benchmarks now and will update the pr description with fresh numbers shortly.

lyne7-sc added a commit to lyne7-sc/datafusion that referenced this pull request May 23, 2026
Comment thread datafusion/functions-nested/src/remove.rs Outdated
Comment thread datafusion/functions-nested/src/remove.rs Outdated
Comment thread datafusion/functions-nested/src/remove.rs Outdated
let mut offsets = Vec::<OffsetSize>::with_capacity(list_array.len() + 1);
offsets.push(OffsetSize::zero());

let mut mutable = MutableArrayData::with_capacities(
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 wonder if an approach using take kernel could provide even more performance gains?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I benchmarked both take and filter kernel approaches against the current MutableArrayData::extend path. Overall, MutableArrayData performs best — on small lists (size=10) take is faster (~10%), possibly by avoiding MutableArrayData initialization overhead, but on medium/large lists (size≥100) MutableArrayData tends to win decisively (take is 60–170% slower depending on type). For variable-length types (strings), the gap appears to widen further.

One possible explanation is that take performs per-index random access for each element, whereas MutableArrayData may instead execute contiguous memcpy operations over memory regions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

But, the current benchmarks use 10% needle density (sparse removals / high retention), which is likely the most common case? For dense removal workloads the trade-offs may shift, take or filter could become competitive when there are fewer contiguous ranges to memcpy.

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.

That makes sense, especially as we're operating on list arrays anyway

Comment thread datafusion/functions-nested/src/remove.rs Outdated
Comment thread datafusion/functions-nested/src/remove.rs Outdated
@Jefffrey
Copy link
Copy Markdown
Contributor

run benchmark array_remove

@adriangbot
Copy link
Copy Markdown

🤖 Criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c4530809483-308-zhrgt 6.12.68+ #1 SMP Wed Apr 1 02:23:28 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/remove (4805bb7) to 265473e (merge-base) diff
BENCH_NAME=array_remove
BENCH_COMMAND=cargo bench --features=parquet --bench array_remove
BENCH_FILTER=
Results will be posted here when complete


File an issue against this benchmark runner

Comment thread datafusion/functions-nested/src/remove.rs Outdated
@adriangbot
Copy link
Copy Markdown

🤖 Criterion 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                                                                    main                                   perf_remove
-----                                                                    ----                                   -----------
array_remove_all_int64/remove/list size: 10, num_rows: 4000              6.03   1498.6±9.00µs        ? ?/sec    1.00    248.5±0.97µs        ? ?/sec
array_remove_all_int64/remove/list size: 100, num_rows: 10000            2.65      7.4±0.09ms        ? ?/sec    1.00      2.8±0.12ms        ? ?/sec
array_remove_all_int64/remove/list size: 500, num_rows: 10000            1.74     22.7±0.26ms        ? ?/sec    1.00     13.1±0.44ms        ? ?/sec
array_remove_all_int64_nested/remove/list size: 10, num_rows: 4000       1.00     12.2±0.12ms        ? ?/sec    1.00     12.3±0.17ms        ? ?/sec
array_remove_all_int64_nested/remove/list size: 100, num_rows: 3000      1.00     68.6±1.10ms        ? ?/sec    1.00     68.5±0.93ms        ? ?/sec
array_remove_all_int64_nested/remove/list size: 300, num_rows: 1500      1.00     98.4±1.25ms        ? ?/sec    1.00     98.5±1.51ms        ? ?/sec
array_remove_boolean/remove/list size: 10, num_rows: 4000                4.05  1338.4±13.49µs        ? ?/sec    1.00    330.1±2.23µs        ? ?/sec
array_remove_boolean/remove/list size: 100, num_rows: 10000              3.08      5.2±0.03ms        ? ?/sec    1.00   1704.0±5.47µs        ? ?/sec
array_remove_boolean/remove/list size: 500, num_rows: 10000              2.28     12.5±0.02ms        ? ?/sec    1.00      5.5±0.02ms        ? ?/sec
array_remove_fixed_size_binary/remove/list size: 10, num_rows: 4000      4.64  1456.4±10.14µs        ? ?/sec    1.00    314.0±1.00µs        ? ?/sec
array_remove_fixed_size_binary/remove/list size: 100, num_rows: 10000    2.01      8.3±0.69ms        ? ?/sec    1.00      4.1±0.74ms        ? ?/sec
array_remove_fixed_size_binary/remove/list size: 500, num_rows: 10000    1.18     39.8±1.07ms        ? ?/sec    1.00     33.8±0.78ms        ? ?/sec
array_remove_int64/remove/list size: 10, num_rows: 4000                  6.15   1469.8±7.25µs        ? ?/sec    1.00    238.8±1.00µs        ? ?/sec
array_remove_int64/remove/list size: 100, num_rows: 10000                4.22      6.2±0.32ms        ? ?/sec    1.00  1465.3±131.70µs        ? ?/sec
array_remove_int64/remove/list size: 500, num_rows: 10000                1.65     22.0±0.17ms        ? ?/sec    1.00     13.3±0.65ms        ? ?/sec
array_remove_int64_nested/remove/list size: 10, num_rows: 4000           1.02     12.4±0.16ms        ? ?/sec    1.00     12.2±0.14ms        ? ?/sec
array_remove_int64_nested/remove/list size: 100, num_rows: 3000          1.00     68.3±1.05ms        ? ?/sec    1.00     68.2±1.09ms        ? ?/sec
array_remove_int64_nested/remove/list size: 300, num_rows: 1500          1.00     97.4±1.58ms        ? ?/sec    1.00     97.7±1.37ms        ? ?/sec
array_remove_n_int64/remove/list size: 10, num_rows: 4000                6.04   1494.8±8.71µs        ? ?/sec    1.00    247.7±1.02µs        ? ?/sec
array_remove_n_int64/remove/list size: 100, num_rows: 10000              3.33      6.4±0.15ms        ? ?/sec    1.00  1929.5±154.35µs        ? ?/sec
array_remove_n_int64/remove/list size: 500, num_rows: 10000              1.93     19.2±0.25ms        ? ?/sec    1.00     10.0±0.76ms        ? ?/sec
array_remove_n_int64_nested/remove/list size: 10, num_rows: 4000         1.03     12.7±0.18ms        ? ?/sec    1.00     12.3±0.19ms        ? ?/sec
array_remove_n_int64_nested/remove/list size: 100, num_rows: 3000        1.00     68.2±0.71ms        ? ?/sec    1.03     70.1±1.92ms        ? ?/sec
array_remove_n_int64_nested/remove/list size: 300, num_rows: 1500        1.00     98.1±1.42ms        ? ?/sec    1.06    103.6±2.89ms        ? ?/sec
array_remove_strings/remove/list size: 10, num_rows: 4000                4.32   1775.8±7.52µs        ? ?/sec    1.00    411.2±1.01µs        ? ?/sec
array_remove_strings/remove/list size: 100, num_rows: 10000              1.70     11.4±0.17ms        ? ?/sec    1.00      6.7±0.13ms        ? ?/sec
array_remove_strings/remove/list size: 500, num_rows: 10000              1.14     46.0±1.05ms        ? ?/sec    1.00     40.5±0.68ms        ? ?/sec

Resource Usage

base (merge-base)

Metric Value
Wall time 315.1s
Peak memory 4.7 GiB
Avg memory 4.6 GiB
CPU user 359.8s
CPU sys 14.8s
Peak spill 0 B

branch

Metric Value
Wall time 300.1s
Peak memory 4.7 GiB
Avg memory 4.5 GiB
CPU user 341.9s
CPU sys 16.1s
Peak spill 0 B

File an issue against this benchmark runner

@lyne7-sc
Copy link
Copy Markdown
Contributor Author

@Jefffrey Addressed all comments. Thank you for the detailed review!

@Jefffrey Jefffrey added this pull request to the merge queue May 25, 2026
@Jefffrey
Copy link
Copy Markdown
Contributor

Thanks @lyne7-sc & @neilconway

Merged via the queue into apache:main with commit a87bdc9 May 25, 2026
35 checks passed
@lyne7-sc lyne7-sc deleted the perf/remove branch May 26, 2026 01:49
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.

4 participants