Skip to content

Commit a997d7f

Browse files
perf(in_list): add BitmapFilter for u8/u16 types
Adds O(1) bitmap-based set membership for 1-byte and 2-byte integer types. Also introduces result.rs with shared logic for building BooleanArray results with correct SQL null propagation. BitmapFilter stores a bitset where bit N is set if value N is in the set: - U8Config: 256 bits = 32 bytes (fits in cache line) - U16Config: 65536 bits = 8 KB (fits in L1 cache) Lookup is a single bit test: `bits[value / 64] & (1 << (value % 64))`. This outperforms both hash lookup and branchless comparison at all list sizes for these small integer types.
1 parent 094df73 commit a997d7f

6 files changed

Lines changed: 395 additions & 164 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
//! Implementation of `InList` expressions: [`InListExpr`]
1919
2020
mod array_filter;
21+
mod primitive;
22+
mod result;
2123
mod strategy;
24+
mod transform;
2225

2326
use std::any::Any;
2427
use std::fmt::Debug;
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//! Optimized primitive type filters for InList expressions
19+
//!
20+
//! This module provides high-performance membership testing for Arrow primitive types.
21+
22+
use arrow::array::{Array, ArrayRef, AsArray, BooleanArray};
23+
use arrow::datatypes::ArrowPrimitiveType;
24+
use datafusion_common::{Result, exec_datafusion_err};
25+
26+
use super::array_filter::StaticFilter;
27+
use super::result::{build_in_list_result, handle_dictionary};
28+
29+
// =============================================================================
30+
// BITMAP FILTERS (O(1) lookup for u8/u16 via bit test)
31+
// =============================================================================
32+
33+
/// Trait for bitmap storage (stack-allocated for u8, heap-allocated for u16).
34+
pub(crate) trait BitmapStorage: Send + Sync {
35+
fn new_zeroed() -> Self;
36+
fn set_bit(&mut self, index: usize);
37+
fn get_bit(&self, index: usize) -> bool;
38+
}
39+
40+
impl BitmapStorage for [u64; 4] {
41+
#[inline]
42+
fn new_zeroed() -> Self {
43+
[0u64; 4]
44+
}
45+
#[inline]
46+
fn set_bit(&mut self, index: usize) {
47+
self[index / 64] |= 1u64 << (index % 64);
48+
}
49+
#[inline(always)]
50+
fn get_bit(&self, index: usize) -> bool {
51+
(self[index / 64] >> (index % 64)) & 1 != 0
52+
}
53+
}
54+
55+
impl BitmapStorage for Box<[u64; 1024]> {
56+
#[inline]
57+
fn new_zeroed() -> Self {
58+
Box::new([0u64; 1024])
59+
}
60+
#[inline]
61+
fn set_bit(&mut self, index: usize) {
62+
self[index / 64] |= 1u64 << (index % 64);
63+
}
64+
#[inline(always)]
65+
fn get_bit(&self, index: usize) -> bool {
66+
(self[index / 64] >> (index % 64)) & 1 != 0
67+
}
68+
}
69+
70+
/// Configuration trait for bitmap filters.
71+
pub(crate) trait BitmapFilterConfig: Send + Sync + 'static {
72+
type Native: arrow::datatypes::ArrowNativeType + Copy + Send + Sync;
73+
type ArrowType: ArrowPrimitiveType<Native = Self::Native>;
74+
type Storage: BitmapStorage;
75+
76+
fn to_index(v: Self::Native) -> usize;
77+
}
78+
79+
/// Config for u8 bitmap (256 bits = 32 bytes, fits in cache line).
80+
pub(crate) enum U8Config {}
81+
impl BitmapFilterConfig for U8Config {
82+
type Native = u8;
83+
type ArrowType = arrow::datatypes::UInt8Type;
84+
type Storage = [u64; 4];
85+
86+
#[inline(always)]
87+
fn to_index(v: u8) -> usize {
88+
v as usize
89+
}
90+
}
91+
92+
/// Config for u16 bitmap (65536 bits = 8 KB, fits in L1 cache).
93+
pub(crate) enum U16Config {}
94+
impl BitmapFilterConfig for U16Config {
95+
type Native = u16;
96+
type ArrowType = arrow::datatypes::UInt16Type;
97+
type Storage = Box<[u64; 1024]>;
98+
99+
#[inline(always)]
100+
fn to_index(v: u16) -> usize {
101+
v as usize
102+
}
103+
}
104+
105+
/// Bitmap filter for O(1) set membership via single bit test.
106+
///
107+
/// For small integer types (u8/u16), bitmap lookup outperforms both branchless
108+
/// and hashed approaches at all list sizes.
109+
pub(crate) struct BitmapFilter<C: BitmapFilterConfig> {
110+
null_count: usize,
111+
bits: C::Storage,
112+
}
113+
114+
impl<C: BitmapFilterConfig> BitmapFilter<C> {
115+
pub(crate) fn try_new(in_array: &ArrayRef) -> Result<Self> {
116+
let prim_array =
117+
in_array.as_primitive_opt::<C::ArrowType>().ok_or_else(|| {
118+
exec_datafusion_err!("BitmapFilter: expected primitive array")
119+
})?;
120+
let mut bits = C::Storage::new_zeroed();
121+
for v in prim_array.iter().flatten() {
122+
bits.set_bit(C::to_index(v));
123+
}
124+
Ok(Self {
125+
null_count: prim_array.null_count(),
126+
bits,
127+
})
128+
}
129+
130+
#[inline(always)]
131+
fn check(&self, needle: C::Native) -> bool {
132+
self.bits.get_bit(C::to_index(needle))
133+
}
134+
135+
/// Check membership using a raw values slice (zero-copy path for type reinterpretation).
136+
#[inline]
137+
pub(crate) fn contains_slice(
138+
&self,
139+
values: &[C::Native],
140+
nulls: Option<&arrow::buffer::NullBuffer>,
141+
negated: bool,
142+
) -> BooleanArray {
143+
build_in_list_result(
144+
values.len(),
145+
nulls,
146+
self.null_count > 0,
147+
negated,
148+
|i| self.check(unsafe { *values.get_unchecked(i) }),
149+
)
150+
}
151+
}
152+
153+
impl<C: BitmapFilterConfig> StaticFilter for BitmapFilter<C> {
154+
fn null_count(&self) -> usize {
155+
self.null_count
156+
}
157+
158+
fn contains(&self, v: &dyn Array, negated: bool) -> Result<BooleanArray> {
159+
handle_dictionary!(self, v, negated);
160+
let v = v.as_primitive_opt::<C::ArrowType>().ok_or_else(|| {
161+
exec_datafusion_err!("BitmapFilter: expected primitive array")
162+
})?;
163+
let input_values = v.values();
164+
Ok(build_in_list_result(
165+
v.len(),
166+
v.nulls(),
167+
self.null_count > 0,
168+
negated,
169+
#[inline(always)]
170+
|i| self.check(unsafe { *input_values.get_unchecked(i) }),
171+
))
172+
}
173+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//! Result building helpers for InList operations
19+
//!
20+
//! This module provides unified logic for building BooleanArray results
21+
//! from IN list membership tests, handling null propagation correctly
22+
//! according to SQL three-valued logic.
23+
24+
use arrow::array::BooleanArray;
25+
use arrow::buffer::{BooleanBuffer, NullBuffer};
26+
27+
// =============================================================================
28+
// RESULT BUILDER FOR IN LIST OPERATIONS
29+
// =============================================================================
30+
//
31+
// Truth table for (needle_nulls, haystack_has_nulls, negated):
32+
// (Some, true, false) → values: valid & contains, nulls: valid & contains
33+
// (None, true, false) → values: contains, nulls: contains
34+
// (Some, true, true) → values: valid ^ (valid & contains), nulls: valid & contains
35+
// (None, true, true) → values: !contains, nulls: contains
36+
// (Some, false, false) → values: valid & contains, nulls: valid
37+
// (Some, false, true) → values: valid & !contains, nulls: valid
38+
// (None, false, false) → values: contains, nulls: none
39+
// (None, false, true) → values: !contains, nulls: none
40+
41+
/// Builds a BooleanArray result for IN list operations.
42+
///
43+
/// This function handles the complex null propagation logic for SQL IN lists:
44+
/// - If the needle value is null, the result is null
45+
/// - If the needle is not in the set AND the haystack has nulls, the result is null
46+
/// - Otherwise, the result is true/false based on membership and negation
47+
#[inline]
48+
pub(crate) fn build_in_list_result<C>(
49+
len: usize,
50+
needle_nulls: Option<&NullBuffer>,
51+
haystack_has_nulls: bool,
52+
negated: bool,
53+
contains: C,
54+
) -> BooleanArray
55+
where
56+
C: FnMut(usize) -> bool,
57+
{
58+
// Pass closure by value to avoid indirection on each call
59+
let contains_buf = BooleanBuffer::collect_bool(len, contains);
60+
build_result_from_contains(needle_nulls, haystack_has_nulls, negated, contains_buf)
61+
}
62+
63+
/// Builds a BooleanArray result from a pre-computed contains buffer.
64+
#[inline]
65+
pub(crate) fn build_result_from_contains(
66+
needle_nulls: Option<&NullBuffer>,
67+
haystack_has_nulls: bool,
68+
negated: bool,
69+
contains_buf: BooleanBuffer,
70+
) -> BooleanArray {
71+
match (needle_nulls, haystack_has_nulls, negated) {
72+
(Some(v), true, false) => {
73+
let buf = v.inner() & &contains_buf;
74+
BooleanArray::new(buf.clone(), Some(NullBuffer::new(buf)))
75+
}
76+
(None, true, false) => {
77+
BooleanArray::new(contains_buf.clone(), Some(NullBuffer::new(contains_buf)))
78+
}
79+
(Some(v), true, true) => {
80+
let nulls = v.inner() & &contains_buf;
81+
BooleanArray::new(v.inner() ^ &nulls, Some(NullBuffer::new(nulls)))
82+
}
83+
(None, true, true) => {
84+
BooleanArray::new(!&contains_buf, Some(NullBuffer::new(contains_buf)))
85+
}
86+
(Some(v), false, false) => {
87+
BooleanArray::new(v.inner() & &contains_buf, Some(v.clone()))
88+
}
89+
(Some(v), false, true) => {
90+
BooleanArray::new(v.inner() & &(!&contains_buf), Some(v.clone()))
91+
}
92+
(None, false, false) => BooleanArray::new(contains_buf, None),
93+
(None, false, true) => BooleanArray::new(!&contains_buf, None),
94+
}
95+
}
96+
97+
// =============================================================================
98+
// DICTIONARY ARRAY HANDLING
99+
// =============================================================================
100+
101+
/// Macro to handle dictionary arrays in StaticFilter::contains implementations.
102+
///
103+
/// This macro extracts the dictionary values, performs the contains check on
104+
/// the values array, and then uses `take` to map the results back to the
105+
/// dictionary keys.
106+
macro_rules! handle_dictionary {
107+
($self:ident, $v:ident, $negated:ident) => {
108+
arrow::array::downcast_dictionary_array! {
109+
$v => {
110+
let values_contains = $self.contains($v.values().as_ref(), $negated)?;
111+
let result = arrow::compute::take(&values_contains, $v.keys(), None)?;
112+
return Ok(arrow::array::downcast_array(result.as_ref()))
113+
}
114+
_ => {}
115+
}
116+
};
117+
}
118+
119+
pub(crate) use handle_dictionary;

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,22 @@ use arrow::datatypes::*;
2727
use datafusion_common::{HashSet, Result, exec_datafusion_err};
2828

2929
use super::array_filter::{ArrayStaticFilter, StaticFilter};
30+
use super::primitive::{U8Config, U16Config};
31+
use super::transform::make_bitmap_filter;
3032

3133
pub(crate) fn instantiate_static_filter(
3234
in_array: ArrayRef,
3335
) -> Result<Arc<dyn StaticFilter + Send + Sync>> {
3436
match in_array.data_type() {
35-
// Integer primitive types
36-
DataType::Int8 => Ok(Arc::new(Int8StaticFilter::try_new(&in_array)?)),
37-
DataType::Int16 => Ok(Arc::new(Int16StaticFilter::try_new(&in_array)?)),
37+
// 1-byte types: use bitmap (256 bits = 32 bytes)
38+
DataType::Int8 | DataType::UInt8 => make_bitmap_filter::<U8Config>(&in_array),
39+
// 2-byte types: use bitmap (65536 bits = 8 KB)
40+
DataType::Int16 | DataType::UInt16 => make_bitmap_filter::<U16Config>(&in_array),
41+
// 4-byte integer types
3842
DataType::Int32 => Ok(Arc::new(Int32StaticFilter::try_new(&in_array)?)),
39-
DataType::Int64 => Ok(Arc::new(Int64StaticFilter::try_new(&in_array)?)),
40-
DataType::UInt8 => Ok(Arc::new(UInt8StaticFilter::try_new(&in_array)?)),
41-
DataType::UInt16 => Ok(Arc::new(UInt16StaticFilter::try_new(&in_array)?)),
4243
DataType::UInt32 => Ok(Arc::new(UInt32StaticFilter::try_new(&in_array)?)),
44+
// 8-byte integer types
45+
DataType::Int64 => Ok(Arc::new(Int64StaticFilter::try_new(&in_array)?)),
4346
DataType::UInt64 => Ok(Arc::new(UInt64StaticFilter::try_new(&in_array)?)),
4447
// Float primitive types (use ordered wrappers for Hash/Eq)
4548
DataType::Float32 => Ok(Arc::new(Float32StaticFilter::try_new(&in_array)?)),
@@ -228,13 +231,10 @@ macro_rules! primitive_static_filter {
228231
};
229232
}
230233

231-
// Generate specialized filters for all integer primitive types
232-
primitive_static_filter!(Int8StaticFilter, Int8Type);
233-
primitive_static_filter!(Int16StaticFilter, Int16Type);
234+
// Generate specialized filters for 4-byte and 8-byte integer primitive types
235+
// (1-byte and 2-byte types use BitmapFilter instead)
234236
primitive_static_filter!(Int32StaticFilter, Int32Type);
235237
primitive_static_filter!(Int64StaticFilter, Int64Type);
236-
primitive_static_filter!(UInt8StaticFilter, UInt8Type);
237-
primitive_static_filter!(UInt16StaticFilter, UInt16Type);
238238
primitive_static_filter!(UInt32StaticFilter, UInt32Type);
239239
primitive_static_filter!(UInt64StaticFilter, UInt64Type);
240240

0 commit comments

Comments
 (0)