IN LIST: index 32- and 64-bit integers by byte - #24181
Closed
geoffreyclaude wants to merge 4 commits into
Closed
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #24181 +/- ##
==========================================
+ Coverage 80.91% 81.07% +0.16%
==========================================
Files 1102 1109 +7
Lines 377102 382850 +5748
Branches 377102 382850 +5748
==========================================
+ Hits 305143 310410 +5267
- Misses 53769 54069 +300
- Partials 18190 18371 +181 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
geoffreyclaude
force-pushed
the
codex/in-list-frozen-set-rewrite
branch
3 times, most recently
from
August 8, 2026 13:29
c093266 to
31b0cb3
Compare
geoffreyclaude
force-pushed
the
codex/in-list-frozen-set-rewrite
branch
from
August 8, 2026 14:44
31b0cb3 to
689cae2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
Note
This description covers only the final commit,
689cae2a42, relative to the combined lower-stack headb0443b6bb0. The earlier commits visible in this stacked PR belong to the PRs above.Rationale for this change
DataFusion builds a reusable static filter when every expression in an
INlist is constant. The lower PRs in this stack already use a branchless comparison chain for short primitive lists. Once that existing path's limit is exceeded—32 non-null list entries forInt32/UInt32, or 16 forInt64/UInt64—the integer filters previously used a general-purpose hash-table lookup for each ordinary primitive-array input value.A hash table works for every value distribution, but an integer constant set often contains a cheaper index: one of the integer's bytes may divide the set into very small groups. If every possible value of that byte identifies at most four list members, membership can be evaluated as:
This replaces hashing and hash-table probing with one table lookup and a fixed amount of comparison work. The index is constructed once with the static filter and reused across input batches.
What changes are included in this PR?
Where this fits in strategy selection
This commit does not change the existing small-list thresholds. For constant
INlists, normal routing remains:Int32,UInt32IntegerSetInt64,UInt64IntegerSetThe branchless cutoff counts non-null list entries before deduplication.
IntegerSetthen deduplicates those entries, so its admission rules below use the number of distinct non-null values.Building the byte index
IntegerSetbuilds the indexed representation as follows:IN-list values into DataFusion's existingHashSet. This removes duplicates without changing membership semantics and also prepares the fallback representation.For example,
0,256,512,768, and1024all have the same lowest byte, so the lowest-byte bucket would need five slots and cannot be used. Their next byte differs, so that byte provides a valid index.The byte is extracted numerically with shifts and a mask rather than read from memory, so selection is independent of machine endianness. Signed values are partitioned by their bit representation, but the final membership checks still compare the original full-width signed or unsigned values.
Padding buckets safely
Lookup always compares four candidates, even when a bucket contains fewer than four real members. To avoid storing a length for every bucket or branching on it, construction initializes every slot with an arbitrary real member of the set, then overwrites the populated slots.
This padding cannot create a false match:
The selected byte only narrows the candidates. All four final checks use complete 32- or 64-bit equality, so sharing a bucket is not itself a match.
Probing the index
For each input value, the indexed path:
|.In simplified form:
Using
|rather than short-circuiting||makes the work independent of which slot matches and exposes a regular, branch-free four-comparison expression that is amenable to compiler auto-vectorization. The implementation requires no target-specific SIMD intrinsics; whether the compiler emits SIMD remains target- and compiler-dependent. Even without vectorization, the probe is bounded to one direct lookup and four comparisons.Hash fallback
IntegerSetretains the already-builtHashSetwhen:The last case matters for correlated or adversarial distributions: having at most 1,024 values is necessary for the fixed table to hold the set, but it is not sufficient for any single byte to partition it well. Because construction creates the
HashSetfirst, fallback reuses it rather than rebuilding the set.Construction and memory cost
The indexed representation contains exactly 1,024 full-width slots: a 4 KiB bucket payload for
Int32/UInt32, or 8 KiB forInt64/UInt64, plus small enum and allocation metadata. This is a fixed cost even when many slots are padding.Construction still pays the previous one-time hash/deduplication cost, then scans the distinct values for at most four or eight candidate byte positions. While constructing an accepted index, peak memory briefly includes both the hash set and the fixed table; the hash set is dropped when the indexed representation is returned. The tradeoff is therefore a small amount of one-time work and bounded memory for cheaper repeated probes.
Preserving
IN/NOT INsemanticsThe index changes only how raw membership is computed. Its membership bitmap is passed to the shared
build_result_from_containshelper, which applies SQL three-valued logic:NULL;TRUEforINandFALSEforNOT IN, even when the list also containsNULL;NULLwhen the list containsNULL; andFALSEforINandTRUEforNOT IN.Duplicate non-null values are removed before indexing, while list nulls are tracked separately, so neither changes the result. Existing handling for sliced arrays and dictionary-encoded inputs is retained; dictionary values are evaluated and then remapped through their keys.
Related primitive-filter cleanup
To plug
IntegerSetinto the existing primitive filters without duplicating result logic, this commit also:Float32/Float64hash-key wrappers with[u8; 4]and[u8; 8]keys.Floats remain hash-backed; they do not use the integer byte index. Their fixed byte-array keys preserve the previous exact bit-pattern equality, including the distinction between
0.0and-0.0and between different NaN payloads.Scope
Int32,UInt32,Int64, andUInt64.IntegerSetby this change.INlists and all other data types keep their existing strategies.Are these changes tested?
Yes. The tests added by the final commit:
IntegerSetmembership withstd::collections::HashSetover empty, small, and larger generated inputs;UInt32list with dictionary-encoded needles for bothINandNOT IN; andFloat32keys preserve exact bit equality for0.0/-0.0and distinct NaN payloads.Are there any user-facing changes?
No. This is an internal representation and evaluation optimization for constant integer
INlists. Query results and public APIs are unchanged.Local benchmark snapshot
This final commit does not modify the benchmark source. Both revisions use the same existing
datafusion/physical-expr/benches/in_list_strategy.rsinherited from the lower stack, so the comparison isolates the integer-filter implementation. The benchmark was built and run in separate target directories after compilation completed:Criterion defaults and central point estimates were used. Filter construction is outside the timed loop, so these measurements cover repeated evaluation rather than index-construction cost. Each timed iteration evaluates 8,192 input rows. Lower is better.
Beforeis the exact combined head of #24088 and #24102 (b0443b6bb0);Afteris this PR (689cae2a42).The nine reported cases improve 1.85x–5.31x, with a 3.32x geometric mean speedup (69.9% less time).
i32 / 64 / missi32 / 64 / 50% hiti32 / 256 / missi32 / 256 / 50% hiti64 / 32 / missi64 / 32 / 50% hiti64 / 128 / missi64 / 128 / 50% hitnullable i32 / 64 / 50% hit / 20% nullThe baseline and this PR were measured without competing Cargo or rustc processes.
These cases cover random full-width signed-integer distributions. The reported set does not measure filter construction, deliberately forced hash fallback, unsigned integers, or a
NULLinside theINlist; the nullable row has nulls in the input array.