Skip to content

Commit 367f08e

Browse files
IN LIST: unify bitmap filter implementations (apache#23035)
## Which issue does this PR close? - Part of apache#19241. - Stacked on apache#23012. - Next in stack: apache#23013. - Extracted from apache#19390. ## Rationale for this change apache#23011 and apache#23012 intentionally introduce the `UInt8` and `UInt16` bitmap filters as concrete implementations. With both widths visible, the shared shape is now clear: each filter builds a fixed-size bitmap from non-null `IN` list values and probes it with the input value's integer bit pattern. This PR factors that duplicated bitmap machinery into one `BitmapFilter<T>`, where `T` is the Arrow primitive type (`UInt8Type` or `UInt16Type`). Arrow remains the source of truth for the native Rust value through `T::Native`; the only extra type-specific piece is the bitmap storage size, supplied by a small private `BitmapFilterType` trait implemented for those two Arrow types. This does not add a new lookup strategy or change which data types use bitmap filters. The next PR uses this shared shape to let same-width signed integers reuse the unsigned bitmap storage. ## What changes are included in this PR? - Adds `BitmapStorage` for fixed-size bitmap backing stores. - Adds `BitmapFilterType`, a private extension trait that supplies the bitmap storage size for `UInt8Type` and `UInt16Type`. - Replaces the concrete `UInt8BitmapFilter` and `UInt16BitmapFilter` implementations with `BitmapFilter<T>`. - Uses Arrow's `T::Native` directly when setting and checking bit positions. - Keeps `UInt8` and `UInt16` routing behavior unchanged. ## Are these changes tested? Yes. - `cargo fmt --all` - `cargo test -p datafusion-physical-expr bitmap_filter_ --lib` - `cargo test -p datafusion-physical-expr in_list_int_types --lib` - `cargo test -p datafusion-physical-expr test_in_list_from_array_type_combinations --lib` - `cargo test -p datafusion-physical-expr test_in_list_dictionary_types --lib` - `cargo clippy -p datafusion-physical-expr --all-targets --all-features -- -D warnings` ## Are there any user-facing changes? No. This is an internal refactor only. <!-- codex-benchmark-start --> ## Benchmark note No local benchmark numbers are included for this PR because it is intended to be a behavior-preserving refactor of the bitmap filter implementation. Benchmarks were not rerun for this stack split. <!-- codex-benchmark-end -->
1 parent 951b821 commit 367f08e

2 files changed

Lines changed: 86 additions & 91 deletions

File tree

datafusion/physical-expr/src/expressions/in_list/primitive_filter.rs

Lines changed: 83 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -29,113 +29,106 @@ use std::hash::{Hash, Hasher};
2929
use super::result::build_in_list_result;
3030
use super::static_filter::{StaticFilter, handle_dictionary};
3131

32-
/// Bitmap filter for O(1) set membership via single bit test.
32+
/// Storage for the bits used by [`BitmapFilter`].
3333
///
34-
/// `UInt8` has only 256 possible values, so the filter stores membership in a
35-
/// 256-bit bitmap instead of using a hash table.
36-
pub(super) struct UInt8BitmapFilter {
37-
null_count: usize,
38-
bits: [u64; 4],
34+
/// `BitmapFilter` represents an `IN` list with one bit for each possible
35+
/// value, so membership checks become direct bit tests. This trait lets the
36+
/// same filter code use different storage sizes for different integer widths.
37+
pub(super) trait BitmapStorage: Send + Sync {
38+
fn new_zeroed() -> Self;
39+
fn set_bit(&mut self, index: usize);
40+
fn get_bit(&self, index: usize) -> bool;
3941
}
4042

41-
impl UInt8BitmapFilter {
42-
pub(super) fn try_new(in_array: &ArrayRef) -> Result<Self> {
43-
let prim_array = in_array.as_primitive_opt::<UInt8Type>().ok_or_else(|| {
44-
exec_datafusion_err!("UInt8BitmapFilter: expected UInt8 array")
45-
})?;
46-
let mut bits = [0u64; 4];
47-
let mut set_bit = |v: u8| {
48-
let index = usize::from(v);
49-
bits[index / 64] |= 1u64 << (index % 64);
50-
};
51-
52-
let values = prim_array.values();
53-
match prim_array.nulls() {
54-
None => {
55-
for &v in values {
56-
set_bit(v);
57-
}
58-
}
59-
Some(nulls) => {
60-
for i in
61-
BitIndexIterator::new(nulls.validity(), nulls.offset(), nulls.len())
62-
{
63-
set_bit(values[i]);
64-
}
65-
}
66-
}
67-
Ok(Self {
68-
null_count: prim_array.null_count(),
69-
bits,
70-
})
43+
// `UInt8` has 256 possible values, 0 through 255. One bit per value takes
44+
// 256 bits, which fits in four `u64` words.
45+
impl BitmapStorage for [u64; 4] {
46+
#[inline]
47+
fn new_zeroed() -> Self {
48+
[0u64; 4]
49+
}
50+
#[inline]
51+
fn set_bit(&mut self, index: usize) {
52+
self[index / 64] |= 1u64 << (index % 64);
7153
}
72-
7354
#[inline(always)]
74-
fn check(&self, needle: u8) -> bool {
75-
let index = needle as usize;
76-
(self.bits[index / 64] >> (index % 64)) & 1 != 0
55+
fn get_bit(&self, index: usize) -> bool {
56+
(self[index / 64] >> (index % 64)) & 1 != 0
7757
}
7858
}
7959

80-
impl StaticFilter for UInt8BitmapFilter {
81-
fn null_count(&self) -> usize {
82-
self.null_count
60+
// `UInt16` has 65,536 possible values. One bit per value takes 65,536 bits,
61+
// which is 1,024 `u64` words, or 8 KiB. Box the array so the filter stores a
62+
// pointer instead of carrying an 8 KiB array inline.
63+
impl BitmapStorage for Box<[u64; 1024]> {
64+
#[inline]
65+
fn new_zeroed() -> Self {
66+
Box::new([0u64; 1024])
8367
}
84-
85-
fn contains(&self, v: &dyn Array, negated: bool) -> Result<BooleanArray> {
86-
handle_dictionary!(self, v, negated);
87-
let v = v.as_primitive_opt::<UInt8Type>().ok_or_else(|| {
88-
exec_datafusion_err!("UInt8BitmapFilter: expected UInt8 array")
89-
})?;
90-
let input_values = v.values();
91-
Ok(build_in_list_result(
92-
v.len(),
93-
v.nulls(),
94-
self.null_count > 0,
95-
negated,
96-
#[inline(always)]
97-
|i| {
98-
// SAFETY: `build_in_list_result` invokes this closure for
99-
// indices in `0..v.len()`, which matches `input_values.len()`.
100-
let needle = unsafe { *input_values.get_unchecked(i) };
101-
self.check(needle)
102-
},
103-
))
68+
#[inline]
69+
fn set_bit(&mut self, index: usize) {
70+
self[index / 64] |= 1u64 << (index % 64);
71+
}
72+
#[inline(always)]
73+
fn get_bit(&self, index: usize) -> bool {
74+
(self[index / 64] >> (index % 64)) & 1 != 0
10475
}
10576
}
10677

107-
/// Bitmap filter for O(1) `UInt16` set membership via single bit test.
78+
/// Arrow primitive types supported by [`BitmapFilter`].
10879
///
109-
/// `UInt16` has 65,536 possible values, so the filter stores membership in an
110-
/// 8 KiB heap-allocated bitmap instead of using a hash table.
111-
pub(super) struct UInt16BitmapFilter {
80+
/// Arrow already defines the Rust value type as `T::Native`. This trait only
81+
/// supplies the bitmap storage size for the two integer domains that are small
82+
/// enough to represent with one bit per possible value.
83+
pub(super) trait BitmapFilterType:
84+
ArrowPrimitiveType + Send + Sync + 'static
85+
{
86+
type Storage: BitmapStorage;
87+
}
88+
89+
/// `UInt8` has 256 possible values, so four `u64` words cover the full domain.
90+
impl BitmapFilterType for UInt8Type {
91+
type Storage = [u64; 4];
92+
}
93+
94+
/// `UInt16` has 65,536 possible values, so 1,024 `u64` words cover the full
95+
/// domain.
96+
impl BitmapFilterType for UInt16Type {
97+
type Storage = Box<[u64; 1024]>;
98+
}
99+
100+
/// `IN` filter backed by one bit per possible value.
101+
///
102+
/// Building the filter scans the non-null values in the IN-list and turns on
103+
/// the bit selected by each value. Evaluating input values checks the same bit
104+
/// position. Null handling and `NOT IN` inversion are handled by
105+
/// `build_in_list_result`.
106+
pub(super) struct BitmapFilter<T: BitmapFilterType> {
112107
null_count: usize,
113-
bits: Box<[u64; 1024]>,
108+
bits: T::Storage,
114109
}
115110

116-
impl UInt16BitmapFilter {
111+
impl<T> BitmapFilter<T>
112+
where
113+
T: BitmapFilterType,
114+
{
117115
pub(super) fn try_new(in_array: &ArrayRef) -> Result<Self> {
118-
let prim_array = in_array.as_primitive_opt::<UInt16Type>().ok_or_else(|| {
119-
exec_datafusion_err!("UInt16BitmapFilter: expected UInt16 array")
116+
let prim_array = in_array.as_primitive_opt::<T>().ok_or_else(|| {
117+
exec_datafusion_err!("BitmapFilter: expected {} array", T::DATA_TYPE)
120118
})?;
121-
let mut bits = Box::new([0u64; 1024]);
122-
let mut set_bit = |v: u16| {
123-
let index = usize::from(v);
124-
bits[index / 64] |= 1u64 << (index % 64);
125-
};
126-
119+
let mut bits = T::Storage::new_zeroed();
127120
let values = prim_array.values();
128121
match prim_array.nulls() {
129122
None => {
130123
for &v in values {
131-
set_bit(v);
124+
bits.set_bit(v.as_usize());
132125
}
133126
}
134127
Some(nulls) => {
135128
for i in
136129
BitIndexIterator::new(nulls.validity(), nulls.offset(), nulls.len())
137130
{
138-
set_bit(values[i]);
131+
bits.set_bit(values[i].as_usize());
139132
}
140133
}
141134
}
@@ -146,21 +139,23 @@ impl UInt16BitmapFilter {
146139
}
147140

148141
#[inline(always)]
149-
fn check(&self, needle: u16) -> bool {
150-
let index = needle as usize;
151-
(self.bits[index / 64] >> (index % 64)) & 1 != 0
142+
fn check(&self, needle: T::Native) -> bool {
143+
self.bits.get_bit(needle.as_usize())
152144
}
153145
}
154146

155-
impl StaticFilter for UInt16BitmapFilter {
147+
impl<T> StaticFilter for BitmapFilter<T>
148+
where
149+
T: BitmapFilterType,
150+
{
156151
fn null_count(&self) -> usize {
157152
self.null_count
158153
}
159154

160155
fn contains(&self, v: &dyn Array, negated: bool) -> Result<BooleanArray> {
161156
handle_dictionary!(self, v, negated);
162-
let v = v.as_primitive_opt::<UInt16Type>().ok_or_else(|| {
163-
exec_datafusion_err!("UInt16BitmapFilter: expected UInt16 array")
157+
let v = v.as_primitive_opt::<T>().ok_or_else(|| {
158+
exec_datafusion_err!("BitmapFilter: expected {} array", T::DATA_TYPE)
164159
})?;
165160
let input_values = v.values();
166161
Ok(build_in_list_result(
@@ -406,7 +401,7 @@ mod tests {
406401
#[test]
407402
fn bitmap_filter_u8_handles_nulls() -> Result<()> {
408403
let haystack: ArrayRef = Arc::new(UInt8Array::from(vec![Some(1), None, Some(3)]));
409-
let filter = UInt8BitmapFilter::try_new(&haystack)?;
404+
let filter = BitmapFilter::<UInt8Type>::try_new(&haystack)?;
410405
let needles = UInt8Array::from(vec![Some(1), Some(2), None, Some(3)]);
411406

412407
assert_contains(&filter, &needles, vec![Some(true), None, None, Some(true)])?;
@@ -421,7 +416,7 @@ mod tests {
421416
#[test]
422417
fn bitmap_filter_u8_handles_dictionary_needles() -> Result<()> {
423418
let haystack: ArrayRef = Arc::new(UInt8Array::from(vec![Some(1), None, Some(3)]));
424-
let filter = UInt8BitmapFilter::try_new(&haystack)?;
419+
let filter = BitmapFilter::<UInt8Type>::try_new(&haystack)?;
425420

426421
let keys = Int8Array::from(vec![Some(0), Some(1), None, Some(2)]);
427422
let values = Arc::new(UInt8Array::from(vec![Some(1), Some(2), Some(3)]));
@@ -438,7 +433,7 @@ mod tests {
438433
Some(1024),
439434
Some(u16::MAX),
440435
]));
441-
let filter = UInt16BitmapFilter::try_new(&haystack)?;
436+
let filter = BitmapFilter::<UInt16Type>::try_new(&haystack)?;
442437
let needles =
443438
UInt16Array::from(vec![Some(0), Some(1), Some(1024), Some(u16::MAX), None]);
444439

datafusion/physical-expr/src/expressions/in_list/strategy.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use std::sync::Arc;
1919

2020
use arrow::array::ArrayRef;
2121
use arrow::compute::cast;
22-
use arrow::datatypes::DataType;
22+
use arrow::datatypes::{DataType, UInt8Type, UInt16Type};
2323
use datafusion_common::Result;
2424

2525
use super::array_static_filter::ArrayStaticFilter;
@@ -42,8 +42,8 @@ pub(super) fn instantiate_static_filter(
4242
DataType::Int16 => Ok(Arc::new(Int16StaticFilter::try_new(&in_array)?)),
4343
DataType::Int32 => Ok(Arc::new(Int32StaticFilter::try_new(&in_array)?)),
4444
DataType::Int64 => Ok(Arc::new(Int64StaticFilter::try_new(&in_array)?)),
45-
DataType::UInt8 => Ok(Arc::new(UInt8BitmapFilter::try_new(&in_array)?)),
46-
DataType::UInt16 => Ok(Arc::new(UInt16BitmapFilter::try_new(&in_array)?)),
45+
DataType::UInt8 => Ok(Arc::new(BitmapFilter::<UInt8Type>::try_new(&in_array)?)),
46+
DataType::UInt16 => Ok(Arc::new(BitmapFilter::<UInt16Type>::try_new(&in_array)?)),
4747
DataType::UInt32 => Ok(Arc::new(UInt32StaticFilter::try_new(&in_array)?)),
4848
DataType::UInt64 => Ok(Arc::new(UInt64StaticFilter::try_new(&in_array)?)),
4949
// Float primitive types (use ordered wrappers for Hash/Eq)

0 commit comments

Comments
 (0)