Skip to content

IN LIST: add keyed frozen set for large primitive lists - #23015

Draft
geoffreyclaude wants to merge 3 commits into
apache:mainfrom
geoffreyclaude:perf/in_list_direct_probe_filter
Draft

IN LIST: add keyed frozen set for large primitive lists#23015
geoffreyclaude wants to merge 3 commits into
apache:mainfrom
geoffreyclaude:perf/in_list_direct_probe_filter

Conversation

@geoffreyclaude

@geoffreyclaude geoffreyclaude commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

#23014 handles small primitive IN lists with direct comparisons. Larger lists still use the generic HashSet path, which stores Arrow array indices and invokes a generic comparator during lookup.

This PR adds a keyed immutable FrozenSet for fixed-width values. Each bucket stores two values directly; only buckets with additional collisions consult a native hashbrown::HashTable overflow table. The first real member is also the empty-slot sentinel, so the primary table stores values without Option wrappers.

This keeps the common probe small and predictable while retaining exact collision handling. Existing optimizer thresholds are unchanged; any threshold change belongs in a dedicated follow-up PR.

What changes are included in this PR?

  • Adds a reusable keyed FrozenSet with two inline values per bucket and native HashTable overflow handling.
  • Routes larger 4- and 8-byte primitive IN lists through PrimitiveFrozenFilter.
  • Uses Arrow physical compatibility when accepting primitive input arrays.
  • Supports u128 values for later representation-based optimizations without enabling general 128-bit primitive routing here.
  • Covers empty sets, duplicates, the sentinel member, forced collisions, slices, dictionaries, nulls, and physical compatibility.

Are these changes tested?

Yes.

  • cargo fmt --all
  • cargo test -p datafusion-physical-expr --lib expressions::in_list
  • cargo clippy --all-targets --all-features -- -D warnings
  • ./dev/rust_lint.sh
  • RUST_BACKTRACE=1 cargo test --profile ci --exclude datafusion-examples --exclude datafusion-benchmarks --exclude datafusion-cli --workspace --lib --tests --bins --features avro,json,backtrace,extended_tests,recursive_protection,parquet_encryption

Are there any user-facing changes?

No. This is an internal performance optimization only.

Local benchmark snapshot

Built and run with release-nonlto, filtered to the relevant large primitive-list rows:

cargo bench -p datafusion-physical-expr --profile release-nonlto --bench in_list_strategy -- <filter> --save-baseline <baseline>

Filters used: primitive/i32/large_list and primitive/i64/large_list.

Method: directly compared Criterion slope point estimates from isolated saved baselines. The expression is constructed outside the timed loop. Lower is better; changes within +/-5% are treated as noise.

Compared baselines: #23014 -> #23015

Relevant scope: the 12 large primitive-list rows shared with #23014.

Summary: 12 relevant rows, 12 faster, 0 slower, 0 within +/-5%. Geometric-mean time decreased by 51.6%.

A separate extension run covered eight added 1K/10K-list rows. All eight improved, with a 31.7% geometric-mean decrease.

Largest relevant deltas:

Benchmark Before After Change
primitive/i64/large_list/list=128/match=50% 57.18 us 14.94 us -73.9% (3.83x faster)
primitive/i32/large_list/list=33/match=50% 48.78 us 14.20 us -70.9% (3.44x faster)
primitive/i64/large_list/list=17/match=50% 42.05 us 13.33 us -68.3% (3.16x faster)
primitive/i64/large_list/list=32/match=50% 41.50 us 15.29 us -63.1% (2.71x faster)
primitive/i32/large_list/list=64/match=50% 42.28 us 16.04 us -62.1% (2.64x faster)
primitive/i32/large_list/list=256/match=50% 39.71 us 18.05 us -54.6% (2.20x faster)
Full relevant table (12 rows)
Benchmark Before After Change
primitive/i32/large_list/list=33/match=0% 23.82 us 12.38 us -48.0% (1.92x faster)
primitive/i32/large_list/list=33/match=50% 48.78 us 14.20 us -70.9% (3.44x faster)
primitive/i32/large_list/list=64/match=0% 23.91 us 13.08 us -45.3% (1.83x faster)
primitive/i32/large_list/list=64/match=50% 42.28 us 16.04 us -62.1% (2.64x faster)
primitive/i32/large_list/list=256/match=0% 18.63 us 13.70 us -26.5% (1.36x faster)
primitive/i32/large_list/list=256/match=50% 39.71 us 18.05 us -54.6% (2.20x faster)
primitive/i64/large_list/list=17/match=0% 17.59 us 15.79 us -10.3% (1.11x faster)
primitive/i64/large_list/list=17/match=50% 42.05 us 13.33 us -68.3% (3.16x faster)
primitive/i64/large_list/list=32/match=0% 19.63 us 17.84 us -9.1% (1.10x faster)
primitive/i64/large_list/list=32/match=50% 41.50 us 15.29 us -63.1% (2.71x faster)
primitive/i64/large_list/list=128/match=0% 24.63 us 15.62 us -36.6% (1.58x faster)
primitive/i64/large_list/list=128/match=50% 57.18 us 14.94 us -73.9% (3.83x faster)

For context, across the 12 cases shared with the original direct-probe implementation, this layout was 4.7% faster overall geometrically: miss-only cases were 11.6% slower, while 50%-hit cases were 18.7% faster.

@github-actions github-actions Bot added the physical-expr Changes to the physical-expr crates label Jun 18, 2026
@geoffreyclaude
geoffreyclaude force-pushed the perf/in_list_direct_probe_filter branch 2 times, most recently from 12ca843 to 0111ce5 Compare June 18, 2026 09:12
@geoffreyclaude geoffreyclaude changed the title Implement Direct Probe (Hash) Filter for large primitive lists IN LIST: add direct-probe hash filter for large primitive lists Jun 18, 2026
@geoffreyclaude
geoffreyclaude force-pushed the perf/in_list_direct_probe_filter branch 3 times, most recently from a109166 to 2e20173 Compare June 19, 2026 05:55
@github-actions github-actions Bot added the auto detected api change Auto detected API change label Jun 19, 2026
@geoffreyclaude
geoffreyclaude force-pushed the perf/in_list_direct_probe_filter branch from 2e20173 to f0c8d4d Compare June 22, 2026 13:50
@github-actions github-actions Bot removed the auto detected api change Auto detected API change label Jun 22, 2026
@geoffreyclaude
geoffreyclaude force-pushed the perf/in_list_direct_probe_filter branch 9 times, most recently from 436582f to 57a8e6f Compare June 26, 2026 16:21
@geoffreyclaude
geoffreyclaude force-pushed the perf/in_list_direct_probe_filter branch 4 times, most recently from 2c36a1f to 6d59f07 Compare July 2, 2026 08:54
@geoffreyclaude
geoffreyclaude force-pushed the perf/in_list_direct_probe_filter branch from 6d59f07 to c7dfb7a Compare July 8, 2026 11:57
@geoffreyclaude
geoffreyclaude force-pushed the perf/in_list_direct_probe_filter branch 2 times, most recently from 218844d to a6f0778 Compare July 8, 2026 13:34
@geoffreyclaude
geoffreyclaude force-pushed the perf/in_list_direct_probe_filter branch from a6f0778 to 15314e6 Compare July 21, 2026 12:16
@geoffreyclaude geoffreyclaude changed the title IN LIST: add direct-probe hash filter for large primitive lists IN LIST: use keyed HashTable for large primitive lists Jul 21, 2026
@codecov-commenter

codecov-commenter commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.16801% with 98 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@7ff7278). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...l-expr/src/expressions/in_list/primitive_filter.rs 83.48% 14 Missing and 39 partials ⚠️
.../physical-expr/src/expressions/in_list/strategy.rs 75.86% 31 Missing and 11 partials ⚠️
...hysical-expr/src/expressions/in_list/frozen_set.rs 97.58% 1 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #23015   +/-   ##
=======================================
  Coverage        ?   80.70%           
=======================================
  Files           ?     1090           
  Lines           ?   369356           
  Branches        ?   369356           
=======================================
  Hits            ?   298090           
  Misses          ?    53480           
  Partials        ?    17786           

☔ 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.

@geoffreyclaude
geoffreyclaude force-pushed the perf/in_list_direct_probe_filter branch from 15314e6 to a8edfdb Compare July 21, 2026 19:24
@geoffreyclaude geoffreyclaude changed the title IN LIST: use keyed HashTable for large primitive lists IN LIST: add keyed frozen set for large primitive lists Jul 21, 2026
pull Bot pushed a commit to TCeason/arrow-datafusion that referenced this pull request Jul 26, 2026
## Which issue does this PR close?

- Part of apache#19241.
- Stacked on [apache#23311](apache#23311).
- Next in stack: apache#23015.
- Extracted from apache#19390.

## Rationale for this change

For very small `IN` lists, building or probing a hash table can be more
work than just comparing the input value with each constant.

For example, for `x IN (10, 20, 30)`, the fast path can behave like:

```text
x == 10 OR x == 20 OR x == 30
```

Because the list is tiny, those comparisons are cheap. The
implementation stores the constants in a fixed-size array and checks
them with a compact comparison chain.

“Branchless” here means the comparisons are combined without stopping at
the first match. That can be faster for these small fixed-width lists
because the CPU gets a predictable sequence of simple operations instead
of hash-table setup and probe logic.

For primitive values that are not already plain unsigned integers, this
PR keeps the logical Arrow type explicit and uses a matching same-width
comparison representation only inside the branchless filter. For
example, `Float16` uses `UInt16` storage, `Float32` uses `UInt32`
storage, and `TimestampNanosecond` uses `UInt64` storage. `Decimal128`
and `IntervalMonthDayNano` use their own 16-byte native representation.
This preserves bit-pattern equality while relying on Arrow's native
primitive compatibility rules: timestamp timezone metadata and
Decimal128 precision/scale metadata may differ, while incompatible
primitive representations remain rejected.

## What changes are included in this PR?

- Adds a const-generic `BranchlessFilter` for small primitive `IN`
lists.
- Adds thresholds for when this path is used:
  - up to 16 values for 1-byte types
  - up to 8 values for 2-byte types
  - up to 32 values for 4-byte types
  - up to 16 values for 8-byte types
  - up to 4 values for 16-byte types
- Keeps dispatch concrete and explicit in `strategy.rs`.
- Maps each optimized logical type to the comparison representation used
by the branchless filter:
  - `Int8` -> `UInt8`
  - `Int16`, `Float16` -> `UInt16`
  - `Int32`, `Float32`, `Date32`, `Time32` -> `UInt32`
- `Int64`, `Float64`, `Date64`, `Time64`, `Timestamp`, `Duration` ->
`UInt64`
- `Decimal128`, `IntervalMonthDayNano` -> their native 16-byte
representation
- Leaves larger 1-byte and 2-byte lists on the existing bitmap filters.
- Leaves larger 4-byte and 8-byte lists on the existing hash/generic
paths.
- Leaves wider primitive types such as `Decimal256` and unsupported
complex types on the generic path.
- Keeps the same `IN` / `NOT IN` null behavior as the rest of the stack.
- Adds focused coverage for branchless null handling, signed boundary
values, slices, Float16/Float32/Float64 bit patterns, compatible
timestamp/Decimal128 metadata, incompatible timestamp units,
IntervalMonthDayNano values, and same-width wrong-type probe rejection.

## Are these changes tested?

Yes.

- `cargo fmt --all -- --check`
- `cargo test -p datafusion-physical-expr expressions::in_list --lib`
- `cargo test -p datafusion-physical-expr --bench in_list_strategy
--no-run`
- `cargo clippy --all-targets --all-features -- -D warnings`

## Are there any user-facing changes?

No. This is an internal performance optimization only.

## Local benchmark snapshot

Built and run with `release-nonlto`, filtered to the relevant small
primitive-list rows:

```bash
cargo bench -p datafusion-physical-expr --profile release-nonlto --bench in_list_strategy -- <filter> --save-baseline <baseline>
```

Filters used: `narrow_integer`, `primitive/i32/small_list`,
`primitive/i64/small_list`, `f32/small_list`, `timestamp_ns/small_list`,
and `interval_month_day_nano/small_list`.

Method: directly compared Criterion's raw sample minima (`min(time /
iterations)`) from `sample.json`. Lower is better; changes within +/-5%
are treated as noise.

Compared baselines:
[apache#23311](apache#23311) ->
[apache#23014](apache#23014)

Relevant scope: small primitive-list rows.

Summary: 39 relevant rows, 28 faster, 0 slower, 11 within +/-5%.

Largest relevant deltas:

| Benchmark | Before | After | Change |
|---|---:|---:|---:|
| `timestamp_ns/small_list/list=4/match=50%` | 46.55 us | 3.17 us |
-93.2% (14.69x faster) |
| `f32/small_list/list=4/match=50%` | 33.93 us | 3.04 us | -91.0%
(11.15x faster) |
| `primitive/i32/small_list/list=4/match=50%` | 32.63 us | 3.08 us |
-90.5% (10.58x faster) |
| `primitive/i64/small_list/list=4/match=50%` | 33.55 us | 3.18 us |
-90.5% (10.54x faster) |
| `timestamp_ns/small_list/list=4/match=0%` | 19.57 us | 3.18 us |
-83.8% (6.16x faster) |
| `f32/small_list/list=4/match=0%` | 18.14 us | 3.05 us | -83.2% (5.95x
faster) |
| `primitive/i32/small_list/list=4/match=0%` | 17.00 us | 3.04 us |
-82.1% (5.59x faster) |
| `primitive/i64/small_list/list=4/match=0%` | 17.12 us | 3.22 us |
-81.2% (5.31x faster) |
| `primitive/i32/small_list/list=16/match=50%/NOT_IN` | 31.98 us | 7.26
us | -77.3% (4.41x faster) |
| `nulls/primitive/i32/small_list/list=16/match=50%/nulls=20%` | 29.35
us | 7.32 us | -75.1% (4.01x faster) |
| `timestamp_ns/small_list/list=16/match=50%` | 45.32 us | 11.79 us |
-74.0% (3.84x faster) |
| `nulls/primitive/i32/small_list/list=16/match=50%/nulls=50%` | 25.89
us | 7.31 us | -71.8% (3.54x faster) |
| `nulls/primitive/i32/small_list/list=16/match=50%/nulls=20%/NOT_IN` |
26.05 us | 7.42 us | -71.5% (3.51x faster) |
| `interval_month_day_nano/small_list/list=4/match=50%` | 52.94 us |
15.52 us | -70.7% (3.41x faster) |
| `f32/small_list/list=32/match=50%` | 38.78 us | 13.27 us | -65.8%
(2.92x faster) |
| `primitive/i64/small_list/list=16/match=50%` | 29.46 us | 11.76 us |
-60.1% (2.50x faster) |

<details>
<summary>Full relevant table (39 rows)</summary>

| Benchmark | Before | After | Change |
|---|---:|---:|---:|
| `narrow_integer/u8/list=4/match=0%` | 3.86 us | 2.79 us | -27.8%
(1.38x faster) |
| `narrow_integer/u8/list=4/match=50%` | 3.84 us | 2.78 us | -27.7%
(1.38x faster) |
| `narrow_integer/u8/list=16/match=0%` | 3.88 us | 3.85 us | -0.8%
(within noise) |
| `narrow_integer/u8/list=16/match=50%` | 3.84 us | 3.86 us | +0.5%
(within noise) |
| `narrow_integer/i16/list=4/match=0%` | 3.93 us | 3.18 us | -19.1%
(1.24x faster) |
| `narrow_integer/i16/list=4/match=50%` | 3.92 us | 3.16 us | -19.5%
(1.24x faster) |
| `narrow_integer/i16/list=64/match=0%` | 3.96 us | 3.82 us | -3.5%
(within noise) |
| `narrow_integer/i16/list=64/match=50%` | 3.91 us | 3.80 us | -2.9%
(within noise) |
| `narrow_integer/i16/list=256/match=0%` | 3.90 us | 3.81 us | -2.5%
(within noise) |
| `narrow_integer/i16/list=256/match=50%` | 3.97 us | 3.81 us | -4.1%
(within noise) |
| `narrow_integer/f16/list=4/match=0%` | 3.87 us | 3.16 us | -18.5%
(1.23x faster) |
| `narrow_integer/f16/list=4/match=50%` | 3.94 us | 3.15 us | -20.2%
(1.25x faster) |
| `narrow_integer/f16/list=64/match=0%` | 3.87 us | 3.84 us | -0.6%
(within noise) |
| `narrow_integer/f16/list=64/match=50%` | 3.93 us | 3.85 us | -1.9%
(within noise) |
| `narrow_integer/f16/list=256/match=0%` | 3.90 us | 3.84 us | -1.5%
(within noise) |
| `narrow_integer/f16/list=256/match=50%` | 3.87 us | 3.91 us | +1.2%
(within noise) |
| `nulls/narrow_integer/u8/list=16/match=50%/nulls=20%` | 3.92 us | 4.02
us | +2.5% (within noise) |
| `primitive/i32/small_list/list=4/match=0%` | 17.00 us | 3.04 us |
-82.1% (5.59x faster) |
| `primitive/i32/small_list/list=4/match=50%` | 32.63 us | 3.08 us |
-90.5% (10.58x faster) |
| `primitive/i32/small_list/list=32/match=0%` | 16.34 us | 13.33 us |
-18.5% (1.23x faster) |
| `primitive/i32/small_list/list=32/match=50%` | 31.17 us | 13.31 us |
-57.3% (2.34x faster) |
| `primitive/i32/small_list/list=16/match=50%/NOT_IN` | 31.98 us | 7.26
us | -77.3% (4.41x faster) |
| `nulls/primitive/i32/small_list/list=16/match=50%/nulls=20%` | 29.35
us | 7.32 us | -75.1% (4.01x faster) |
| `nulls/primitive/i32/small_list/list=16/match=50%/nulls=20%/NOT_IN` |
26.05 us | 7.42 us | -71.5% (3.51x faster) |
| `nulls/primitive/i32/small_list/list=16/match=50%/nulls=50%` | 25.89
us | 7.31 us | -71.8% (3.54x faster) |
| `primitive/i64/small_list/list=4/match=0%` | 17.12 us | 3.22 us |
-81.2% (5.31x faster) |
| `primitive/i64/small_list/list=4/match=50%` | 33.55 us | 3.18 us |
-90.5% (10.54x faster) |
| `primitive/i64/small_list/list=16/match=0%` | 16.34 us | 11.93 us |
-27.0% (1.37x faster) |
| `primitive/i64/small_list/list=16/match=50%` | 29.46 us | 11.76 us |
-60.1% (2.50x faster) |
| `f32/small_list/list=4/match=0%` | 18.14 us | 3.05 us | -83.2% (5.95x
faster) |
| `f32/small_list/list=4/match=50%` | 33.93 us | 3.04 us | -91.0%
(11.15x faster) |
| `f32/small_list/list=32/match=0%` | 22.05 us | 13.43 us | -39.1%
(1.64x faster) |
| `f32/small_list/list=32/match=50%` | 38.78 us | 13.27 us | -65.8%
(2.92x faster) |
| `timestamp_ns/small_list/list=4/match=0%` | 19.57 us | 3.18 us |
-83.8% (6.16x faster) |
| `timestamp_ns/small_list/list=4/match=50%` | 46.55 us | 3.17 us |
-93.2% (14.69x faster) |
| `timestamp_ns/small_list/list=16/match=0%` | 19.73 us | 12.07 us |
-38.8% (1.63x faster) |
| `timestamp_ns/small_list/list=16/match=50%` | 45.32 us | 11.79 us |
-74.0% (3.84x faster) |
| `interval_month_day_nano/small_list/list=4/match=0%` | 20.12 us |
13.20 us | -34.4% (1.52x faster) |
| `interval_month_day_nano/small_list/list=4/match=50%` | 52.94 us |
15.52 us | -70.7% (3.41x faster) |

</details>

---------

Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

physical-expr Changes to the physical-expr crates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants