Skip to content

Commit cc752d0

Browse files
Implement Zero-Copy Reinterpretation and enable Int8/Int16 Bitmaps
Introduces zero-copy buffer reinterpretation to allow signed integers and other 1 or 2-byte primitive types (e.g. Float16) to use the high-performance bitmap filters. Triggers for all types with 1-byte or 2-byte width.
1 parent 55f3836 commit cc752d0

5 files changed

Lines changed: 218 additions & 12 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ mod primitive_filter;
4141
mod result;
4242
mod static_filter;
4343
mod strategy;
44+
mod transform;
4445

4546
use static_filter::StaticFilter;
4647
use strategy::instantiate_static_filter;

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,19 @@ impl<C: BitmapFilterConfig> BitmapFilter<C> {
131131
fn check(&self, needle: C::Native) -> bool {
132132
self.bits.get_bit(C::to_index(needle))
133133
}
134+
135+
/// Check membership using a raw values slice (zero-copy path for type reinterpretation).
136+
#[inline]
137+
pub(super) fn contains_slice(
138+
&self,
139+
values: &[C::Native],
140+
nulls: Option<&NullBuffer>,
141+
negated: bool,
142+
) -> BooleanArray {
143+
build_in_list_result(values.len(), nulls, self.null_count > 0, negated, |i| {
144+
self.check(unsafe { *values.get_unchecked(i) })
145+
})
146+
}
134147
}
135148

136149
impl<C: BitmapFilterConfig> StaticFilter for BitmapFilter<C> {
@@ -345,9 +358,6 @@ macro_rules! primitive_static_filter {
345358
};
346359
}
347360

348-
// Generate specialized filters for all integer primitive types
349-
primitive_static_filter!(Int8StaticFilter, Int8Type);
350-
primitive_static_filter!(Int16StaticFilter, Int16Type);
351361
primitive_static_filter!(Int32StaticFilter, Int32Type);
352362
primitive_static_filter!(Int64StaticFilter, Int64Type);
353363
primitive_static_filter!(UInt32StaticFilter, UInt32Type);

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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
//! from IN list membership tests, handling null propagation correctly
2222
//! according to SQL three-valued logic.
2323
24+
#![expect(dead_code)]
25+
2426
use arrow::array::BooleanArray;
2527
use arrow::buffer::{BooleanBuffer, NullBuffer};
2628

@@ -43,6 +45,9 @@ use arrow::buffer::{BooleanBuffer, NullBuffer};
4345
///
4446
/// This version computes contains for all positions, including nulls, then applies
4547
/// null masking via bitmap operations.
48+
///
49+
/// For expensive contains checks (like ByteViewMaskedFilter with string comparison),
50+
/// use `build_in_list_result_with_null_shortcircuit` instead.
4651
#[inline]
4752
pub(crate) fn build_in_list_result<C>(
4853
len: usize,
@@ -58,6 +63,106 @@ where
5863
build_result_from_contains(needle_nulls, haystack_has_nulls, negated, contains_buf)
5964
}
6065

66+
/// Builds a BooleanArray result with null short-circuit (optimized for expensive contains).
67+
///
68+
/// Unlike `build_in_list_result`, this version checks nulls INSIDE the loop and
69+
/// skips the contains check for null positions. This is optimal for expensive
70+
/// contains checks (like ByteViewMaskedFilter with hash lookup + string comparison) where
71+
/// skipping lookups outweighs the branch overhead.
72+
///
73+
/// The shortcircuit is only applied when `needle_null_count > 0` - if there are
74+
/// no actual nulls, we avoid the branch overhead entirely.
75+
///
76+
/// Use this for: ByteViewMaskedFilter, Utf8TwoStageFilter (string/binary types)
77+
/// Use `build_in_list_result` for: DirectProbeFilter, BranchlessFilter (primitive types)
78+
#[inline]
79+
pub(crate) fn build_in_list_result_with_null_shortcircuit<C>(
80+
len: usize,
81+
needle_nulls: Option<&NullBuffer>,
82+
needle_null_count: usize,
83+
haystack_has_nulls: bool,
84+
negated: bool,
85+
mut contains: C,
86+
) -> BooleanArray
87+
where
88+
C: FnMut(usize) -> bool,
89+
{
90+
// When null_count=0, treat as no validity buffer to avoid extra work.
91+
// The validity buffer might exist but have all bits set to true.
92+
let effective_nulls = needle_nulls.filter(|_| needle_null_count > 0);
93+
94+
match effective_nulls {
95+
Some(nulls) => {
96+
// Has nulls: check validity inside loop to skip expensive contains()
97+
let contains_buf =
98+
BooleanBuffer::collect_bool(len, |i| nulls.is_valid(i) && contains(i));
99+
build_result_from_contains_premasked(
100+
Some(nulls),
101+
haystack_has_nulls,
102+
negated,
103+
contains_buf,
104+
)
105+
}
106+
None => {
107+
// No nulls: compute contains for all positions without branch overhead
108+
let contains_buf = BooleanBuffer::collect_bool(len, contains);
109+
// Use premasked path since contains_buf is "trivially premasked" (no nulls to mask)
110+
build_result_from_contains_premasked(
111+
None,
112+
haystack_has_nulls,
113+
negated,
114+
contains_buf,
115+
)
116+
}
117+
}
118+
}
119+
120+
/// Builds result from a contains buffer that was pre-masked at null positions.
121+
///
122+
/// This is used by `build_in_list_result_with_null_shortcircuit` where the
123+
/// contains buffer already has `false` at null positions due to the short-circuit.
124+
///
125+
/// Since contains_buf is pre-masked (false at null positions), we can simplify:
126+
/// - `valid & contains_buf` = `contains_buf` (already 0 where valid is 0)
127+
/// - XOR can replace AND+NOT for the negated case: `valid ^ contains = valid & !contains`
128+
#[inline]
129+
fn build_result_from_contains_premasked(
130+
needle_nulls: Option<&NullBuffer>,
131+
haystack_has_nulls: bool,
132+
negated: bool,
133+
contains_buf: BooleanBuffer,
134+
) -> BooleanArray {
135+
match (needle_nulls, haystack_has_nulls, negated) {
136+
// Haystack has nulls: result is null unless value is found
137+
(_, true, false) => {
138+
// contains_buf is already masked (false at null positions)
139+
BooleanArray::new(contains_buf.clone(), Some(NullBuffer::new(contains_buf)))
140+
}
141+
(Some(v), true, true) => {
142+
// NOT IN with nulls: true if valid and not found, null if found or needle null
143+
// XOR: valid ^ contains = 1 iff valid=1 and contains=0 (not found)
144+
BooleanArray::new(
145+
v.inner() ^ &contains_buf,
146+
Some(NullBuffer::new(contains_buf)),
147+
)
148+
}
149+
(None, true, true) => {
150+
BooleanArray::new(!&contains_buf, Some(NullBuffer::new(contains_buf)))
151+
}
152+
// Haystack has no nulls: result validity follows needle validity
153+
(Some(v), false, false) => {
154+
// contains_buf is already masked, just use needle validity for nulls
155+
BooleanArray::new(contains_buf, Some(v.clone()))
156+
}
157+
(Some(v), false, true) => {
158+
// Need AND because !contains_buf is 1 at null positions
159+
BooleanArray::new(v.inner() & &(!&contains_buf), Some(v.clone()))
160+
}
161+
(None, false, false) => BooleanArray::new(contains_buf, None),
162+
(None, false, true) => BooleanArray::new(!&contains_buf, None),
163+
}
164+
}
165+
61166
/// Builds a BooleanArray result from a pre-computed contains buffer.
62167
///
63168
/// This version does not assume contains_buf is pre-masked at null positions.

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use datafusion_common::Result;
2525
use super::array_static_filter::ArrayStaticFilter;
2626
use super::primitive_filter::*;
2727
use super::static_filter::StaticFilter;
28+
use super::transform::make_bitmap_filter;
2829

2930
pub(super) fn instantiate_static_filter(
3031
in_array: ArrayRef,
@@ -37,17 +38,14 @@ pub(super) fn instantiate_static_filter(
3738
_ => in_array,
3839
};
3940
match in_array.data_type() {
40-
// Integer primitive types
41-
DataType::Int8 => Ok(Arc::new(Int8StaticFilter::try_new(&in_array)?)),
42-
DataType::Int16 => Ok(Arc::new(Int16StaticFilter::try_new(&in_array)?)),
41+
DataType::Int8 | DataType::UInt8 => {
42+
make_bitmap_filter::<UInt8BitmapConfig>(&in_array)
43+
}
44+
DataType::Int16 | DataType::UInt16 => {
45+
make_bitmap_filter::<UInt16BitmapConfig>(&in_array)
46+
}
4347
DataType::Int32 => Ok(Arc::new(Int32StaticFilter::try_new(&in_array)?)),
4448
DataType::Int64 => Ok(Arc::new(Int64StaticFilter::try_new(&in_array)?)),
45-
DataType::UInt8 => Ok(Arc::new(BitmapFilter::<UInt8BitmapConfig>::try_new(
46-
&in_array,
47-
)?)),
48-
DataType::UInt16 => Ok(Arc::new(BitmapFilter::<UInt16BitmapConfig>::try_new(
49-
&in_array,
50-
)?)),
5149
DataType::UInt32 => Ok(Arc::new(UInt32StaticFilter::try_new(&in_array)?)),
5250
DataType::UInt64 => Ok(Arc::new(UInt64StaticFilter::try_new(&in_array)?)),
5351
// Float primitive types (use ordered wrappers for Hash/Eq)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
//! Type transformation utilities for InList filters
19+
//!
20+
//! This module provides type reinterpretation for optimizing filter dispatch.
21+
//! For equality comparison, only the bit pattern matters, so we can:
22+
//! - Reinterpret signed integers as unsigned (Int32 → UInt32)
23+
//! - Reinterpret floats as unsigned integers (Float64 → UInt64)
24+
//!
25+
//! This allows using a single filter implementation (e.g., for UInt64) to handle
26+
//! multiple types (Int64, Float64, Timestamp, Duration) that share the same
27+
//! byte width, reducing code duplication.
28+
29+
use std::sync::Arc;
30+
31+
use arrow::array::{Array, ArrayRef, BooleanArray, PrimitiveArray};
32+
use arrow::buffer::ScalarBuffer;
33+
use arrow::datatypes::ArrowPrimitiveType;
34+
use datafusion_common::Result;
35+
36+
use super::primitive_filter::{BitmapFilter, BitmapFilterConfig};
37+
use super::static_filter::{StaticFilter, handle_dictionary};
38+
39+
// =============================================================================
40+
// REINTERPRETING FILTERS (zero-copy type conversion)
41+
// =============================================================================
42+
43+
/// Reinterpreting filter for bitmap lookups (u8/u16).
44+
struct ReinterpretedBitmap<C: BitmapFilterConfig> {
45+
inner: BitmapFilter<C>,
46+
}
47+
48+
impl<C: BitmapFilterConfig> StaticFilter for ReinterpretedBitmap<C> {
49+
fn null_count(&self) -> usize {
50+
self.inner.null_count()
51+
}
52+
53+
fn contains(&self, v: &dyn Array, negated: bool) -> Result<BooleanArray> {
54+
handle_dictionary!(self, v, negated);
55+
56+
let data = v.to_data();
57+
let values: &[C::Native] = data.buffer::<C::Native>(0);
58+
59+
Ok(self.inner.contains_slice(values, data.nulls(), negated))
60+
}
61+
}
62+
63+
/// Reinterprets any primitive-like array as the target primitive type T by extracting
64+
/// the underlying buffer.
65+
///
66+
/// This is a zero-copy operation that works for all primitive types (Int*, UInt*, Float*,
67+
/// Timestamp*, Date*, Duration*, etc.) by directly accessing the underlying buffer,
68+
/// ignoring any metadata like timezones or precision/scale.
69+
#[inline]
70+
pub(crate) fn reinterpret_any_primitive_to<T: ArrowPrimitiveType>(
71+
array: &dyn Array,
72+
) -> ArrayRef {
73+
let values = array.to_data().buffers()[0].clone();
74+
let buffer: ScalarBuffer<T::Native> = values.into();
75+
Arc::new(PrimitiveArray::<T>::new(buffer, array.nulls().cloned()))
76+
}
77+
78+
/// Creates a bitmap filter for u8/u16 types, reinterpreting if needed.
79+
pub(crate) fn make_bitmap_filter<C>(
80+
in_array: &ArrayRef,
81+
) -> Result<Arc<dyn StaticFilter + Send + Sync>>
82+
where
83+
C: BitmapFilterConfig,
84+
{
85+
if in_array.data_type() == &C::ArrowType::DATA_TYPE {
86+
return Ok(Arc::new(BitmapFilter::<C>::try_new(in_array)?));
87+
}
88+
89+
let reinterpreted = reinterpret_any_primitive_to::<C::ArrowType>(in_array.as_ref());
90+
let inner = BitmapFilter::<C>::try_new(&reinterpreted)?;
91+
Ok(Arc::new(ReinterpretedBitmap { inner }))
92+
}

0 commit comments

Comments
 (0)