Skip to content

Commit 5351b95

Browse files
Implement Bitmap Filter for UInt8 (Stack-based)
Replaces HashSet<u8> with a 32-byte stack-allocated bitmap. Provides O(1) membership testing via bit-shifting, significantly reducing memory overhead and improving cache locality. Triggers for UInt8 arrays.
1 parent a84579d commit 5351b95

3 files changed

Lines changed: 149 additions & 24 deletions

File tree

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

Lines changed: 131 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,77 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
use arrow::array::{
19-
Array, ArrayRef, AsArray, BooleanArray, downcast_array, downcast_dictionary_array,
20-
};
18+
//! Optimized primitive type filters for InList expressions.
19+
//!
20+
//! This module provides membership tests for Arrow primitive types.
21+
22+
use arrow::array::{Array, ArrayRef, AsArray, BooleanArray};
2123
use arrow::buffer::{BooleanBuffer, NullBuffer};
22-
use arrow::compute::take;
2324
use arrow::datatypes::*;
2425
use datafusion_common::{HashSet, Result, exec_datafusion_err};
2526
use std::hash::{Hash, Hasher};
2627

27-
use super::static_filter::StaticFilter;
28+
use super::result::build_in_list_result;
29+
use super::static_filter::{StaticFilter, handle_dictionary};
30+
31+
/// Bitmap filter for O(1) set membership via single bit test.
32+
///
33+
/// `UInt8` has only 256 possible values, so the filter stores membership in a
34+
/// 256-bit bitmap instead of using a hash table.
35+
pub(super) struct UInt8BitmapFilter {
36+
null_count: usize,
37+
bits: [u64; 4],
38+
}
39+
40+
impl UInt8BitmapFilter {
41+
pub(super) fn try_new(in_array: &ArrayRef) -> Result<Self> {
42+
let prim_array = in_array.as_primitive_opt::<UInt8Type>().ok_or_else(|| {
43+
exec_datafusion_err!("UInt8BitmapFilter: expected UInt8 array")
44+
})?;
45+
let mut bits = [0u64; 4];
46+
for v in prim_array.iter().flatten() {
47+
let index = v as usize;
48+
bits[index / 64] |= 1u64 << (index % 64);
49+
}
50+
Ok(Self {
51+
null_count: prim_array.null_count(),
52+
bits,
53+
})
54+
}
55+
56+
#[inline(always)]
57+
fn check(&self, needle: u8) -> bool {
58+
let index = needle as usize;
59+
(self.bits[index / 64] >> (index % 64)) & 1 != 0
60+
}
61+
}
62+
63+
impl StaticFilter for UInt8BitmapFilter {
64+
fn null_count(&self) -> usize {
65+
self.null_count
66+
}
67+
68+
fn contains(&self, v: &dyn Array, negated: bool) -> Result<BooleanArray> {
69+
handle_dictionary!(self, v, negated);
70+
let v = v.as_primitive_opt::<UInt8Type>().ok_or_else(|| {
71+
exec_datafusion_err!("UInt8BitmapFilter: expected UInt8 array")
72+
})?;
73+
let input_values = v.values();
74+
Ok(build_in_list_result(
75+
v.len(),
76+
v.nulls(),
77+
self.null_count > 0,
78+
negated,
79+
#[inline(always)]
80+
|i| {
81+
// SAFETY: `build_in_list_result` invokes this closure for
82+
// indices in `0..v.len()`, which matches `input_values.len()`.
83+
let needle = unsafe { *input_values.get_unchecked(i) };
84+
self.check(needle)
85+
},
86+
))
87+
}
88+
}
2889

2990
/// Wrapper for f32 that implements Hash and Eq using bit comparison.
3091
/// This treats NaN values as equal to each other when they have the same bit pattern.
@@ -94,9 +155,13 @@ macro_rules! primitive_static_filter {
94155

95156
impl $Name {
96157
pub(super) fn try_new(in_array: &ArrayRef) -> Result<Self> {
97-
let in_array = in_array
98-
.as_primitive_opt::<$ArrowType>()
99-
.ok_or_else(|| exec_datafusion_err!("Failed to downcast an array to a '{}' array", stringify!($ArrowType)))?;
158+
let in_array =
159+
in_array.as_primitive_opt::<$ArrowType>().ok_or_else(|| {
160+
exec_datafusion_err!(
161+
"Failed to downcast an array to a '{}' array",
162+
stringify!($ArrowType)
163+
)
164+
})?;
100165

101166
let mut values = HashSet::with_capacity(in_array.len());
102167
let null_count = in_array.null_count();
@@ -115,19 +180,14 @@ macro_rules! primitive_static_filter {
115180
}
116181

117182
fn contains(&self, v: &dyn Array, negated: bool) -> Result<BooleanArray> {
118-
// Handle dictionary arrays by recursing on the values
119-
downcast_dictionary_array! {
120-
v => {
121-
let values_contains = self.contains(v.values().as_ref(), negated)?;
122-
let result = take(&values_contains, v.keys(), None)?;
123-
return Ok(downcast_array(result.as_ref()))
124-
}
125-
_ => {}
126-
}
183+
handle_dictionary!(self, v, negated);
127184

128-
let v = v
129-
.as_primitive_opt::<$ArrowType>()
130-
.ok_or_else(|| exec_datafusion_err!("Failed to downcast an array to a '{}' array", stringify!($ArrowType)))?;
185+
let v = v.as_primitive_opt::<$ArrowType>().ok_or_else(|| {
186+
exec_datafusion_err!(
187+
"Failed to downcast an array to a '{}' array",
188+
stringify!($ArrowType)
189+
)
190+
})?;
131191

132192
let haystack_has_nulls = self.null_count > 0;
133193
let needle_values = v.values();
@@ -188,8 +248,10 @@ macro_rules! primitive_static_filter {
188248
}
189249
(true, true) => {
190250
// Both have nulls - combine needle nulls with haystack-induced nulls
191-
let needle_validity = needle_nulls.map(|n| n.inner().clone())
192-
.unwrap_or_else(|| BooleanBuffer::new_set(needle_values.len()));
251+
let needle_validity =
252+
needle_nulls.map(|n| n.inner().clone()).unwrap_or_else(
253+
|| BooleanBuffer::new_set(needle_values.len()),
254+
);
193255

194256
// Valid when original "in set" is true (see above)
195257
let haystack_validity = if negated {
@@ -215,7 +277,6 @@ primitive_static_filter!(Int8StaticFilter, Int8Type);
215277
primitive_static_filter!(Int16StaticFilter, Int16Type);
216278
primitive_static_filter!(Int32StaticFilter, Int32Type);
217279
primitive_static_filter!(Int64StaticFilter, Int64Type);
218-
primitive_static_filter!(UInt8StaticFilter, UInt8Type);
219280
primitive_static_filter!(UInt16StaticFilter, UInt16Type);
220281
primitive_static_filter!(UInt32StaticFilter, UInt32Type);
221282
primitive_static_filter!(UInt64StaticFilter, UInt64Type);
@@ -231,3 +292,50 @@ macro_rules! float_static_filter {
231292
// Generate specialized filters for float types using ordered wrappers
232293
float_static_filter!(Float32StaticFilter, Float32Type, OrderedFloat32);
233294
float_static_filter!(Float64StaticFilter, Float64Type, OrderedFloat64);
295+
296+
#[cfg(test)]
297+
mod tests {
298+
use super::*;
299+
use std::sync::Arc;
300+
301+
use arrow::array::{DictionaryArray, Int8Array, UInt8Array};
302+
303+
fn assert_contains(
304+
filter: &UInt8BitmapFilter,
305+
needles: &dyn Array,
306+
expected: Vec<Option<bool>>,
307+
) -> Result<()> {
308+
assert_eq!(
309+
filter.contains(needles, false)?,
310+
BooleanArray::from(expected)
311+
);
312+
Ok(())
313+
}
314+
315+
#[test]
316+
fn bitmap_filter_u8_handles_nulls() -> Result<()> {
317+
let haystack: ArrayRef = Arc::new(UInt8Array::from(vec![Some(1), None, Some(3)]));
318+
let filter = UInt8BitmapFilter::try_new(&haystack)?;
319+
let needles = UInt8Array::from(vec![Some(1), Some(2), None, Some(3)]);
320+
321+
assert_contains(&filter, &needles, vec![Some(true), None, None, Some(true)])?;
322+
assert_eq!(
323+
filter.contains(&needles, true)?,
324+
BooleanArray::from(vec![Some(false), None, None, Some(false)])
325+
);
326+
327+
Ok(())
328+
}
329+
330+
#[test]
331+
fn bitmap_filter_u8_handles_dictionary_needles() -> Result<()> {
332+
let haystack: ArrayRef = Arc::new(UInt8Array::from(vec![Some(1), None, Some(3)]));
333+
let filter = UInt8BitmapFilter::try_new(&haystack)?;
334+
335+
let keys = Int8Array::from(vec![Some(0), Some(1), None, Some(2)]);
336+
let values = Arc::new(UInt8Array::from(vec![Some(1), Some(2), Some(3)]));
337+
let needles = DictionaryArray::try_new(keys, values)?;
338+
339+
assert_contains(&filter, &needles, vec![Some(true), None, None, Some(true)])
340+
}
341+
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,20 @@ pub(super) trait StaticFilter {
3535
/// implementation unwraps the dictionary and operates on its values.
3636
fn contains(&self, v: &dyn Array, negated: bool) -> Result<BooleanArray>;
3737
}
38+
39+
/// Evaluate dictionary-encoded needles by applying a filter to dictionary
40+
/// values and remapping the result through the keys.
41+
macro_rules! handle_dictionary {
42+
($self:ident, $v:ident, $negated:ident) => {
43+
arrow::array::downcast_dictionary_array! {
44+
$v => {
45+
let values_contains = $self.contains($v.values().as_ref(), $negated)?;
46+
let result = arrow::compute::take(&values_contains, $v.keys(), None)?;
47+
return Ok(arrow::array::downcast_array(result.as_ref()))
48+
}
49+
_ => {}
50+
}
51+
};
52+
}
53+
54+
pub(super) use handle_dictionary;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ 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(UInt8StaticFilter::try_new(&in_array)?)),
45+
DataType::UInt8 => Ok(Arc::new(UInt8BitmapFilter::try_new(&in_array)?)),
4646
DataType::UInt16 => Ok(Arc::new(UInt16StaticFilter::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)?)),

0 commit comments

Comments
 (0)