Skip to content

Commit a17cf88

Browse files
refactor: expand signed zeros before selecting IN-list filters
Add the missing signed-zero encoding once in the shared static-filter factory after dictionary flattening. Select the filter using the expanded physical non-null list length. Remove per-filter zero insertion and extra-length branchless kernels. Cover both signs across cutoffs and preserve nulls, NaN payloads and slices.
1 parent afb3f72 commit a17cf88

3 files changed

Lines changed: 152 additions & 142 deletions

File tree

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

Lines changed: 20 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,8 @@
3838
//! `Float32` and a `UInt32` both use four bytes per value. The filter compares
3939
//! those stored bits through an unsigned type of the same size, without copying
4040
//! the value buffer. A bit pattern is simply the bytes Arrow uses to store a
41-
//! value. Float comparisons treat `0.0` and `-0.0` as equal under SQL
42-
//! semantics, while preserving distinctions between different NaN values.
43-
//! [`BranchlessFilterType`] defines these safe,
41+
//! value. Comparing it preserves details such as `0.0` versus `-0.0` and
42+
//! different NaN values. [`BranchlessFilterType`] defines these safe,
4443
//! same-sized mappings and checks their sizes at compile time.
4544
//!
4645
//! The fast path is limited to short lists:
@@ -76,7 +75,6 @@ use arrow::buffer::{BooleanBuffer, ScalarBuffer};
7675
use arrow::datatypes::*;
7776
use arrow::util::bit_iterator::BitIndexIterator;
7877
use datafusion_common::{Result, exec_datafusion_err, internal_datafusion_err};
79-
use half::f16;
8078

8179
use super::result::build_result_from_contains;
8280
use super::static_filter::StaticFilter;
@@ -119,24 +117,20 @@ const BRANCHLESS_MAX_16B: usize = 4;
119117
///
120118
/// `T` is the logical Arrow type accepted by the filter. `CompareType` is the
121119
/// same-width type used for the fixed comparison chain. Signed integers,
122-
/// floats, and temporal values use an unsigned comparison type. Most values
123-
/// compare by raw bit pattern; floats additionally treat both signed-zero
124-
/// patterns as equal.
120+
/// floats, and temporal values use an unsigned comparison type so they compare
121+
/// by their raw bit pattern.
125122
pub(super) trait BranchlessFilterType:
126-
ArrowPrimitiveType + Send + Sync + Sized + 'static
123+
ArrowPrimitiveType + Send + Sync + 'static
127124
{
128125
type CompareType: ArrowPrimitiveType + Send + Sync + 'static;
129126

130127
/// Maximum number of non-null IN-list values to handle with
131128
/// [`BranchlessFilter`] for this primitive type.
132129
const MAX_LIST_LEN: usize;
133-
134-
/// The two signed-zero encodings for float types.
135-
const SIGNED_ZERO_BITS: Option<[BranchlessNative<Self>; 2]> = None;
136130
}
137131

138132
macro_rules! branchless_filter_type {
139-
($logical:ty, $compare:ty, $max_len:expr $(, $zero_bits:expr)?) => {
133+
($logical:ty, $compare:ty, $max_len:expr) => {
140134
// The branchless filter reads the same Arrow value buffer as the
141135
// comparison type. That is only valid when both native types have the
142136
// same width, so catch any bad mapping here at compile time.
@@ -149,7 +143,6 @@ macro_rules! branchless_filter_type {
149143
impl BranchlessFilterType for $logical {
150144
type CompareType = $compare;
151145
const MAX_LIST_LEN: usize = $max_len;
152-
$(const SIGNED_ZERO_BITS: Option<[BranchlessNative<Self>; 2]> = Some($zero_bits);)?
153146
}
154147
};
155148
}
@@ -158,33 +151,18 @@ branchless_filter_type!(Int8Type, UInt8Type, BRANCHLESS_MAX_1B);
158151
branchless_filter_type!(UInt8Type, UInt8Type, BRANCHLESS_MAX_1B);
159152
branchless_filter_type!(Int16Type, UInt16Type, BRANCHLESS_MAX_2B);
160153
branchless_filter_type!(UInt16Type, UInt16Type, BRANCHLESS_MAX_2B);
161-
branchless_filter_type!(
162-
Float16Type,
163-
UInt16Type,
164-
BRANCHLESS_MAX_2B,
165-
[f16::ZERO.to_bits(), f16::NEG_ZERO.to_bits()]
166-
);
154+
branchless_filter_type!(Float16Type, UInt16Type, BRANCHLESS_MAX_2B);
167155

168156
branchless_filter_type!(Int32Type, UInt32Type, BRANCHLESS_MAX_4B);
169157
branchless_filter_type!(UInt32Type, UInt32Type, BRANCHLESS_MAX_4B);
170-
branchless_filter_type!(
171-
Float32Type,
172-
UInt32Type,
173-
BRANCHLESS_MAX_4B,
174-
[0.0_f32.to_bits(), (-0.0_f32).to_bits()]
175-
);
158+
branchless_filter_type!(Float32Type, UInt32Type, BRANCHLESS_MAX_4B);
176159
branchless_filter_type!(Date32Type, UInt32Type, BRANCHLESS_MAX_4B);
177160
branchless_filter_type!(Time32SecondType, UInt32Type, BRANCHLESS_MAX_4B);
178161
branchless_filter_type!(Time32MillisecondType, UInt32Type, BRANCHLESS_MAX_4B);
179162

180163
branchless_filter_type!(Int64Type, UInt64Type, BRANCHLESS_MAX_8B);
181164
branchless_filter_type!(UInt64Type, UInt64Type, BRANCHLESS_MAX_8B);
182-
branchless_filter_type!(
183-
Float64Type,
184-
UInt64Type,
185-
BRANCHLESS_MAX_8B,
186-
[0.0_f64.to_bits(), (-0.0_f64).to_bits()]
187-
);
165+
branchless_filter_type!(Float64Type, UInt64Type, BRANCHLESS_MAX_8B);
188166
branchless_filter_type!(Date64Type, UInt64Type, BRANCHLESS_MAX_8B);
189167
branchless_filter_type!(Time64MicrosecondType, UInt64Type, BRANCHLESS_MAX_8B);
190168
branchless_filter_type!(Time64NanosecondType, UInt64Type, BRANCHLESS_MAX_8B);
@@ -240,10 +218,8 @@ where
240218
}
241219

242220
let all_values = branchless_values::<T>(in_array);
243-
// Float zero may add its other signed encoding.
244-
let mut in_list_values = Vec::with_capacity(
245-
non_null_count + usize::from(T::SIGNED_ZERO_BITS.is_some()),
246-
);
221+
let mut in_list_values = Vec::with_capacity(non_null_count);
222+
247223
match in_array.nulls() {
248224
None => {
249225
in_list_values.extend(all_values.iter().copied());
@@ -258,18 +234,6 @@ where
258234
}
259235

260236
debug_assert_eq!(in_list_values.len(), non_null_count);
261-
262-
// Add the other signed zero once so per-row lookup stays branch-free.
263-
if let Some([positive_zero, negative_zero]) = T::SIGNED_ZERO_BITS {
264-
match (
265-
in_list_values.contains(&positive_zero),
266-
in_list_values.contains(&negative_zero),
267-
) {
268-
(true, false) => in_list_values.push(negative_zero),
269-
(false, true) => in_list_values.push(positive_zero),
270-
_ => {}
271-
}
272-
}
273237
let in_list_values = in_list_values.into_boxed_slice();
274238
let check_values = membership_check_for_len::<T>(in_list_values.len());
275239

@@ -326,23 +290,13 @@ where
326290
T: BranchlessFilterType,
327291
BranchlessNative<T>: Copy + PartialEq,
328292
{
329-
/// Selects a fixed-size comparison function for the enclosing `len` and `T`.
330-
///
331-
/// Arguments must enumerate every length from zero through the logical limit.
332-
/// Their count is therefore one past that limit: the extra length supported
333-
/// for floats when adding the other signed-zero encoding.
334-
///
335-
/// The function is selected once during filter construction.
336293
macro_rules! choose {
337-
($($n:literal),+ $(,)?) => {{
338-
const EXTRA_LEN: usize = [$($n),+].len();
294+
($($n:literal),* $(,)?) => {
339295
match len {
340-
$($n => check_values::<BranchlessNative<T>, $n>,)+
341-
EXTRA_LEN if T::SIGNED_ZERO_BITS.is_some() =>
342-
check_values::<BranchlessNative<T>, EXTRA_LEN>,
296+
$($n => check_values::<BranchlessNative<T>, $n>,)*
343297
_ => unreachable!("list length exceeds the configured limit"),
344298
}
345-
}};
299+
};
346300
}
347301

348302
// Avoid creating checks for lengths a type does not support.
@@ -497,11 +451,11 @@ mod tests {
497451

498452
assert_eq!(
499453
filter.contains(&needles, false)?,
500-
BooleanArray::from(vec![Some(true), Some(true), Some(true), None, None])
454+
BooleanArray::from(vec![None, Some(true), Some(true), None, None])
501455
);
502456
assert_eq!(
503457
filter.contains(&needles, true)?,
504-
BooleanArray::from(vec![Some(false), Some(false), Some(false), None, None])
458+
BooleanArray::from(vec![None, Some(false), Some(false), None, None])
505459
);
506460

507461
let wrong_type = UInt16Array::from(vec![Some(0x8000), Some(0x7e01)]);
@@ -512,26 +466,20 @@ mod tests {
512466
}
513467

514468
#[test]
515-
fn branchless_filter_floats_use_sql_zero_equality() -> Result<()> {
469+
fn branchless_filter_floats_use_bit_equality() -> Result<()> {
516470
let nan_a = f32::from_bits(0x7fc0_0001);
517471
let nan_b = f32::from_bits(0x7fc0_0002);
518472
let haystack: ArrayRef =
519-
Arc::new(Float32Array::from(vec![Some(0.0), Some(nan_a)]));
473+
Arc::new(Float32Array::from(vec![Some(-0.0), Some(nan_a)]));
520474
let filter = BranchlessFilter::<Float32Type>::try_new(&haystack)?;
521475
let needles =
522476
Float32Array::from(vec![Some(0.0), Some(-0.0), Some(nan_a), Some(nan_b)]);
523477

524478
assert_eq!(
525479
filter.contains(&needles, false)?,
526-
BooleanArray::from(vec![Some(true), Some(true), Some(true), Some(false)])
480+
BooleanArray::from(vec![Some(false), Some(true), Some(true), Some(false)])
527481
);
528482

529-
// A list containing both encodings is not expanded.
530-
let zero_only: ArrayRef =
531-
Arc::new(Float32Array::from(vec![Some(0.0), Some(-0.0)]));
532-
let filter = BranchlessFilter::<Float32Type>::try_new(&zero_only)?;
533-
assert_eq!(filter.in_list_values.len(), 2);
534-
535483
let nan_a = f64::from_bits(0x7ff8_0000_0000_0001);
536484
let nan_b = f64::from_bits(0x7ff8_0000_0000_0002);
537485
let haystack: ArrayRef =
@@ -542,7 +490,7 @@ mod tests {
542490

543491
assert_eq!(
544492
filter.contains(&needles, false)?,
545-
BooleanArray::from(vec![Some(true), Some(true), Some(true), Some(false)])
493+
BooleanArray::from(vec![Some(false), Some(true), Some(true), Some(false)])
546494
);
547495

548496
Ok(())

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

Lines changed: 7 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ use arrow::array::{Array, ArrayRef, AsArray, BooleanArray};
2727
use arrow::datatypes::*;
2828
use arrow::util::bit_iterator::BitIndexIterator;
2929
use datafusion_common::{HashSet, Result, exec_datafusion_err};
30-
use half::f16;
3130

3231
use super::branchless_filter::{BranchlessFilter, BranchlessFilterType};
3332
use super::result::build_in_list_result;
@@ -200,9 +199,6 @@ trait BitmapFilterType: ArrowPrimitiveType + Send + Sync + 'static {
200199

201200
/// Returns the index in the bitmap to check for this value.
202201
fn index(value: Self::Native) -> usize;
203-
204-
/// Bitmap indices for the two signed-zero encodings of a float type.
205-
const SIGNED_ZERO_INDICES: Option<[usize; 2]> = None;
206202
}
207203

208204
/// `Int8` has 256 possible bit patterns, so four `u64` words cover the full domain.
@@ -258,11 +254,6 @@ impl BitmapFilterType for Float16Type {
258254
fn index(value: Self::Native) -> usize {
259255
value.to_bits() as usize
260256
}
261-
262-
const SIGNED_ZERO_INDICES: Option<[usize; 2]> = Some([
263-
f16::ZERO.to_bits() as usize,
264-
f16::NEG_ZERO.to_bits() as usize,
265-
]);
266257
}
267258

268259
/// `IN` filter backed by one bit per possible value.
@@ -300,13 +291,6 @@ where
300291
}
301292
}
302293
}
303-
// Store both signed zeros so per-row lookup stays branch-free.
304-
if let Some([positive_zero, negative_zero]) = T::SIGNED_ZERO_INDICES
305-
&& (bits.get_bit(positive_zero) || bits.get_bit(negative_zero))
306-
{
307-
bits.set_bit(positive_zero);
308-
bits.set_bit(negative_zero);
309-
}
310294
Ok(Self {
311295
null_count: prim_array.null_count(),
312296
bits,
@@ -348,13 +332,6 @@ where
348332
}
349333
}
350334

351-
/// Hash keys and the two signed-zero encodings for float types.
352-
trait HashSetKey<V>: From<V> + Eq + Hash + Sized {
353-
const SIGNED_ZERO_KEYS: Option<[Self; 2]> = None;
354-
}
355-
356-
impl<T> HashSetKey<T> for T where T: Copy + Eq + Hash {}
357-
358335
/// Wrapper for f32 that implements Hash and Eq using bit comparison.
359336
/// This treats NaN values as equal to each other when they have the same bit pattern.
360337
#[derive(Clone, Copy)]
@@ -380,10 +357,6 @@ impl From<f32> for OrderedFloat32 {
380357
}
381358
}
382359

383-
impl HashSetKey<f32> for OrderedFloat32 {
384-
const SIGNED_ZERO_KEYS: Option<[Self; 2]> = Some([Self(0.0), Self(-0.0)]);
385-
}
386-
387360
/// Wrapper for f64 that implements Hash and Eq using bit comparison.
388361
/// This treats NaN values as equal to each other when they have the same bit pattern.
389362
#[derive(Clone, Copy)]
@@ -409,10 +382,6 @@ impl From<f64> for OrderedFloat64 {
409382
}
410383
}
411384

412-
impl HashSetKey<f64> for OrderedFloat64 {
413-
const SIGNED_ZERO_KEYS: Option<[Self; 2]> = Some([Self(0.0), Self(-0.0)]);
414-
}
415-
416385
/// Hash-set membership for primitive types.
417386
///
418387
/// `K` defaults to the Arrow type's native value. Floats use an ordered wrapper
@@ -430,7 +399,7 @@ impl<T, K> PrimitiveHashSetFilter<T, K>
430399
where
431400
T: ArrowPrimitiveType,
432401
T::Native: Copy,
433-
K: HashSetKey<T::Native>,
402+
K: From<T::Native> + Eq + Hash,
434403
{
435404
fn try_new(in_array: &ArrayRef) -> Result<Self> {
436405
let in_array = in_array.as_primitive_opt::<T>().ok_or_else(|| {
@@ -444,13 +413,6 @@ where
444413
values.insert(K::from(value));
445414
}
446415

447-
// Store both signed zeros so per-row lookup stays branch-free.
448-
if let Some([positive_zero, negative_zero]) = K::SIGNED_ZERO_KEYS
449-
&& (values.contains(&positive_zero) || values.contains(&negative_zero))
450-
{
451-
values.insert(positive_zero);
452-
values.insert(negative_zero);
453-
}
454416
Ok(Self {
455417
null_count: in_array.null_count(),
456418
values,
@@ -497,8 +459,9 @@ mod tests {
497459

498460
use arrow::array::{
499461
DictionaryArray, Float16Array, Float32Array, Float64Array, Int8Array, Int16Array,
500-
PrimitiveArray, UInt8Array, UInt16Array, UInt32Array,
462+
UInt8Array, UInt16Array, UInt32Array,
501463
};
464+
use half::f16;
502465

503466
use super::super::dictionary_filter::DictionaryFilter;
504467

@@ -544,28 +507,6 @@ mod tests {
544507
Ok(())
545508
}
546509

547-
#[test]
548-
fn branchless_float_zero_expansion_handles_max_list_len() -> Result<()> {
549-
fn assert_routed_filter<T: BranchlessFilterType>(
550-
negative_zero: T::Native,
551-
positive_zero: T::Native,
552-
) -> Result<()> {
553-
// The mirror zero is appended after the full logical list.
554-
let haystack: ArrayRef = Arc::new(PrimitiveArray::<T>::from_value(
555-
negative_zero,
556-
T::MAX_LIST_LEN,
557-
));
558-
let filter = instantiate_branchless_filter(&haystack)?
559-
.expect("a full float list still uses a branchless filter");
560-
let needles = PrimitiveArray::<T>::from_value(positive_zero, 1);
561-
assert_contains(filter.as_ref(), &needles, vec![Some(true)])
562-
}
563-
564-
assert_routed_filter::<Float16Type>(f16::NEG_ZERO, f16::ZERO)?;
565-
assert_routed_filter::<Float32Type>(-0.0, 0.0)?;
566-
assert_routed_filter::<Float64Type>(-0.0, 0.0)
567-
}
568-
569510
#[test]
570511
fn primitive_hash_filter_handles_float_keys() -> Result<()> {
571512
let nan32 = f32::NAN;
@@ -583,14 +524,14 @@ mod tests {
583524
assert_contains(
584525
&filter,
585526
&needles,
586-
vec![Some(true), Some(true), Some(true), Some(false), None],
527+
vec![Some(true), Some(false), Some(true), Some(false), None],
587528
)?;
588529

589530
let nan64 = f64::NAN;
590-
let haystack: ArrayRef = Arc::new(Float64Array::from(vec![-0.0, nan64]));
531+
let haystack: ArrayRef = Arc::new(Float64Array::from(vec![1.0, nan64]));
591532
let filter =
592533
PrimitiveHashSetFilter::<Float64Type, OrderedFloat64>::try_new(&haystack)?;
593-
let needles = Float64Array::from(vec![Some(0.0), Some(nan64), Some(2.0)]);
534+
let needles = Float64Array::from(vec![Some(1.0), Some(nan64), Some(2.0)]);
594535
assert_contains(&filter, &needles, vec![Some(true), Some(true), Some(false)])
595536
}
596537

@@ -719,8 +660,8 @@ mod tests {
719660
);
720661
let filter = BitmapFilter::<Float16Type>::try_new(&haystack)?;
721662
let needles = Float16Array::from(vec![
722-
Some(f16::from_f32(-0.0)),
723663
Some(f16::from_f32(0.0)),
664+
Some(f16::from_f32(-0.0)),
724665
Some(nan_a),
725666
Some(nan_b),
726667
None,

0 commit comments

Comments
 (0)