Skip to content

IN LIST: add branchless filter for small primitive lists#23014

Draft
geoffreyclaude wants to merge 2 commits into
apache:mainfrom
geoffreyclaude:perf/in_list_branchless_filter
Draft

IN LIST: add branchless filter for small primitive lists#23014
geoffreyclaude wants to merge 2 commits into
apache:mainfrom
geoffreyclaude:perf/in_list_branchless_filter

Conversation

@geoffreyclaude

@geoffreyclaude geoffreyclaude commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

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:

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.

Larger primitive lists are left for #23015, where a purpose-built hash lookup becomes the better tradeoff.

For non-unsigned primitive values, this PR keeps the logical Arrow type explicit and uses the matching unsigned same-width physical representation only inside the branchless filter. For example, Float16 uses UInt16 storage, Float32 uses UInt32 storage, and TimestampNanosecond uses UInt64 storage. This preserves bit-pattern equality while keeping construction and probing tied to the original logical type.

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
  • Keeps dispatch concrete and explicit in strategy.rs.
  • Maps each optimized non-unsigned logical type to its matching unsigned same-width branchless representation:
    • Int8 -> UInt8
    • Int16, Float16 -> UInt16
    • Int32, Float32, Date32, Time32 -> UInt32
    • Int64, Float64, Date64, Time64, Timestamp, Duration -> UInt64
  • 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 16-byte, decimal, and interval 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, 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:

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, and timestamp_ns/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: #23311 -> #23014

Relevant scope: small primitive-list rows.

Summary: 37 relevant rows, 26 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)
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)
Full relevant table (37 rows)
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)

@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_branchless_filter branch 3 times, most recently from 428e3cd to eae4046 Compare June 18, 2026 09:03
@geoffreyclaude geoffreyclaude changed the title Implement Branchless Filter for small primitive lists IN LIST: add branchless filter for small primitive lists Jun 18, 2026
@geoffreyclaude geoffreyclaude force-pushed the perf/in_list_branchless_filter branch from eae4046 to 3e3651b Compare June 19, 2026 05:11
@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_branchless_filter branch from 3e3651b to 6a1869f Compare June 19, 2026 05:35
@github-actions github-actions Bot removed the auto detected api change Auto detected API change label Jun 19, 2026
@geoffreyclaude geoffreyclaude force-pushed the perf/in_list_branchless_filter branch 5 times, most recently from bf69ed2 to e040466 Compare June 24, 2026 20:49
@geoffreyclaude geoffreyclaude force-pushed the perf/in_list_branchless_filter branch 8 times, most recently from 04ca744 to f89d3b6 Compare June 30, 2026 07:08
@geoffreyclaude geoffreyclaude force-pushed the perf/in_list_branchless_filter branch from f89d3b6 to 409b0cb Compare July 2, 2026 08:54
@geoffreyclaude geoffreyclaude force-pushed the perf/in_list_branchless_filter branch 4 times, most recently from 95965f5 to 9158a1a Compare July 6, 2026 14:29
@geoffreyclaude geoffreyclaude force-pushed the perf/in_list_branchless_filter branch from 9158a1a to 8a1184e Compare July 6, 2026 14:30
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.

1 participant