Skip to content

Commit f4ec013

Browse files
authored
Merge branch 'main' into qizhu/parquet-pushdown-adaptive-gate
2 parents 0edc760 + 479a0aa commit f4ec013

2 files changed

Lines changed: 238 additions & 39 deletions

File tree

datafusion/functions/benches/nanvl.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,80 @@ fn criterion_benchmark(c: &mut Criterion) {
108108
bench.iter(|| black_box(nanvl_fn.invoke_with_args(args.clone()).unwrap()))
109109
});
110110
}
111+
112+
// Partially-null array benchmarks exercise the null-aware match arms that
113+
// the fully-populated benchmarks above never reach: only-x-null,
114+
// only-y-null, and both-null.
115+
let bench_pair =
116+
|c: &mut Criterion, name: &str, x: ArrayRef, y: ArrayRef, size: usize| {
117+
c.bench_function(name, |bench| {
118+
let args = ScalarFunctionArgs {
119+
args: vec![
120+
ColumnarValue::Array(Arc::clone(&x)),
121+
ColumnarValue::Array(Arc::clone(&y)),
122+
],
123+
arg_fields: vec![
124+
Field::new("a", DataType::Float64, true).into(),
125+
Field::new("b", DataType::Float64, true).into(),
126+
],
127+
number_rows: size,
128+
return_field: Field::new("f", DataType::Float64, true).into(),
129+
config_options: Arc::clone(&config_options),
130+
};
131+
bench.iter(|| black_box(nanvl_fn.invoke_with_args(args.clone()).unwrap()))
132+
});
133+
};
134+
135+
for size in [1024, 4096, 8192] {
136+
// `x` mixes non-NaN, NaN, and null so every code path is taken.
137+
let x_nulls: ArrayRef = Arc::new(Float64Array::from(
138+
(0..size)
139+
.map(|i| match i % 3 {
140+
0 => Some(1.0),
141+
1 => Some(f64::NAN),
142+
_ => None,
143+
})
144+
.collect::<Vec<_>>(),
145+
));
146+
// `x` without nulls, alternating non-NaN and NaN.
147+
let x_full: ArrayRef = Arc::new(Float64Array::from(
148+
(0..size)
149+
.map(|i| if i % 2 == 0 { 1.0 } else { f64::NAN })
150+
.collect::<Vec<_>>(),
151+
));
152+
// `y` with roughly a quarter nulls.
153+
let y_nulls: ArrayRef = Arc::new(Float64Array::from(
154+
(0..size)
155+
.map(|i| if i % 4 == 3 { None } else { Some(2.0) })
156+
.collect::<Vec<_>>(),
157+
));
158+
let y_full: ArrayRef = Arc::new(Float64Array::from(vec![2.0; size]));
159+
160+
// (Some, None): only `x` has nulls.
161+
bench_pair(
162+
c,
163+
&format!("nanvl/array_f64_x_nulls/{size}"),
164+
Arc::clone(&x_nulls),
165+
Arc::clone(&y_full),
166+
size,
167+
);
168+
// (None, Some): only `y` has nulls.
169+
bench_pair(
170+
c,
171+
&format!("nanvl/array_f64_y_nulls/{size}"),
172+
Arc::clone(&x_full),
173+
Arc::clone(&y_nulls),
174+
size,
175+
);
176+
// (Some, Some): both inputs have nulls.
177+
bench_pair(
178+
c,
179+
&format!("nanvl/array_f64_both_nulls/{size}"),
180+
Arc::clone(&x_nulls),
181+
Arc::clone(&y_nulls),
182+
size,
183+
);
184+
}
111185
}
112186

113187
criterion_group!(benches, criterion_benchmark);

datafusion/functions/src/math/nanvl.rs

Lines changed: 164 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,20 @@
1717

1818
use std::sync::Arc;
1919

20-
use arrow::array::{ArrayRef, AsArray, Float16Array, Float32Array, Float64Array};
20+
use arrow::array::builder::NullBufferBuilder;
21+
use arrow::array::{Array, ArrayRef, AsArray, PrimitiveArray};
2122
use arrow::datatypes::DataType::{Float16, Float32, Float64};
22-
use arrow::datatypes::{DataType, Float16Type, Float32Type, Float64Type};
23+
use arrow::datatypes::{
24+
ArrowPrimitiveType, DataType, Float16Type, Float32Type, Float64Type,
25+
};
2326
use datafusion_common::types::{NativeType, logical_float64};
2427
use datafusion_common::{Result, ScalarValue, exec_err, utils::take_function_args};
2528
use datafusion_expr::{
2629
Coercion, ColumnarValue, Documentation, ScalarFunctionArgs, ScalarUDFImpl, Signature,
2730
TypeSignature, TypeSignatureClass, Volatility,
2831
};
2932
use datafusion_macros::user_doc;
33+
use num_traits::Float;
3034

3135
#[user_doc(
3236
doc_section(label = "Math Functions"),
@@ -148,46 +152,80 @@ fn scalar_is_nan(scalar: &ScalarValue) -> bool {
148152
/// - otherwise -> output is x (which may itself be NULL)
149153
fn nanvl(args: &[ArrayRef]) -> Result<ArrayRef> {
150154
match args[0].data_type() {
151-
Float64 => {
152-
let x = args[0].as_primitive::<Float64Type>();
153-
let y = args[1].as_primitive::<Float64Type>();
154-
let result: Float64Array = x
155-
.iter()
156-
.zip(y.iter())
157-
.map(|(x_value, y_value)| match x_value {
158-
Some(x_value) if x_value.is_nan() => y_value,
159-
_ => x_value,
160-
})
161-
.collect();
162-
Ok(Arc::new(result) as ArrayRef)
163-
}
164-
Float32 => {
165-
let x = args[0].as_primitive::<Float32Type>();
166-
let y = args[1].as_primitive::<Float32Type>();
167-
let result: Float32Array = x
155+
Float64 => Ok(Arc::new(nanvl_impl::<Float64Type>(
156+
args[0].as_primitive(),
157+
args[1].as_primitive(),
158+
))),
159+
Float32 => Ok(Arc::new(nanvl_impl::<Float32Type>(
160+
args[0].as_primitive(),
161+
args[1].as_primitive(),
162+
))),
163+
Float16 => Ok(Arc::new(nanvl_impl::<Float16Type>(
164+
args[0].as_primitive(),
165+
args[1].as_primitive(),
166+
))),
167+
other => exec_err!("Unsupported data type {other:?} for function nanvl"),
168+
}
169+
}
170+
171+
/// Element-wise `nanvl`: selects `y[i]` where `x[i]` is `NaN`, otherwise `x[i]`
172+
/// (a null `x` selects `x`, i.e. propagates null).
173+
///
174+
/// This produces output identical to collecting an iterator of `Option`s but
175+
/// splits out a null-free fast path that iterates the raw value slices,
176+
/// skipping per-element validity checks and `Option` handling. The null-aware
177+
/// path builds its null buffer lazily via [`NullBufferBuilder`].
178+
fn nanvl_impl<T>(x: &PrimitiveArray<T>, y: &PrimitiveArray<T>) -> PrimitiveArray<T>
179+
where
180+
T: ArrowPrimitiveType,
181+
T::Native: Float,
182+
{
183+
let xv = x.values();
184+
let yv = y.values();
185+
186+
match (x.nulls(), y.nulls()) {
187+
// No nulls in either input means no nulls in the output, so we can
188+
// iterate values directly and avoid the null bookkeeping entirely.
189+
(None, None) => {
190+
let values: Vec<T::Native> = xv
168191
.iter()
169-
.zip(y.iter())
170-
.map(|(x_value, y_value)| match x_value {
171-
Some(x_value) if x_value.is_nan() => y_value,
172-
_ => x_value,
173-
})
192+
.zip(yv.iter())
193+
.map(
194+
|(&x_value, &y_value)| {
195+
if x_value.is_nan() { y_value } else { x_value }
196+
},
197+
)
174198
.collect();
175-
Ok(Arc::new(result) as ArrayRef)
199+
PrimitiveArray::<T>::new(values.into(), None)
176200
}
177-
Float16 => {
178-
let x = args[0].as_primitive::<Float16Type>();
179-
let y = args[1].as_primitive::<Float16Type>();
180-
let result: Float16Array = x
181-
.iter()
182-
.zip(y.iter())
183-
.map(|(x_value, y_value)| match x_value {
184-
Some(x_value) if x_value.is_nan() => y_value,
185-
_ => x_value,
186-
})
187-
.collect();
188-
Ok(Arc::new(result) as ArrayRef)
201+
_ => {
202+
let len = x.len();
203+
let mut nulls = NullBufferBuilder::new(len);
204+
let mut values = Vec::with_capacity(len);
205+
for i in 0..len {
206+
// `y` is only consulted when `x` is a (non-null) NaN, matching
207+
// the original short-circuiting match.
208+
if x.is_valid(i) {
209+
let x_value = xv[i];
210+
if x_value.is_nan() {
211+
if y.is_valid(i) {
212+
values.push(yv[i]);
213+
nulls.append_non_null();
214+
} else {
215+
values.push(T::Native::default());
216+
nulls.append_null();
217+
}
218+
} else {
219+
values.push(x_value);
220+
nulls.append_non_null();
221+
}
222+
} else {
223+
values.push(T::Native::default());
224+
nulls.append_null();
225+
}
226+
}
227+
PrimitiveArray::<T>::new(values.into(), nulls.finish())
189228
}
190-
other => exec_err!("Unsupported data type {other:?} for function nanvl"),
191229
}
192230
}
193231

@@ -197,7 +235,7 @@ mod test {
197235

198236
use crate::math::nanvl::nanvl;
199237

200-
use arrow::array::{ArrayRef, Float32Array, Float64Array};
238+
use arrow::array::{Array, ArrayRef, Float32Array, Float64Array};
201239
use datafusion_common::cast::{as_float32_array, as_float64_array};
202240

203241
#[test]
@@ -235,4 +273,91 @@ mod test {
235273
assert_eq!(floats.value(2), 3.0);
236274
assert!(floats.value(3).is_nan());
237275
}
276+
277+
#[test]
278+
fn test_nanvl_f64_with_nulls() {
279+
// Covers the null-aware path and null propagation:
280+
// - x null -> null (regardless of y)
281+
// - x NaN, y non-null -> y
282+
// - x NaN, y null -> null
283+
// - x non-NaN -> x
284+
let args: Vec<ArrayRef> = vec![
285+
Arc::new(Float64Array::from(vec![
286+
None,
287+
Some(f64::NAN),
288+
Some(f64::NAN),
289+
Some(2.5),
290+
])), // x
291+
Arc::new(Float64Array::from(vec![
292+
Some(9.0),
293+
Some(6.0),
294+
None,
295+
Some(7.0),
296+
])), // y
297+
];
298+
299+
let result = nanvl(&args).expect("failed to initialize function nanvl");
300+
let floats =
301+
as_float64_array(&result).expect("failed to initialize function nanvl");
302+
303+
assert_eq!(floats.len(), 4);
304+
assert!(floats.is_null(0));
305+
assert_eq!(floats.value(1), 6.0);
306+
assert!(floats.is_null(2));
307+
assert_eq!(floats.value(3), 2.5);
308+
}
309+
310+
#[test]
311+
fn test_nanvl_f64_only_y_nulls() {
312+
// `x` has no nulls, `y` does:
313+
// - x non-NaN -> x
314+
// - x NaN, y non-null -> y
315+
// - x NaN, y null -> null (propagated from y)
316+
let args: Vec<ArrayRef> = vec![
317+
Arc::new(Float64Array::from(vec![1.0, f64::NAN, f64::NAN, 4.0])), // x
318+
Arc::new(Float64Array::from(vec![
319+
Some(5.0),
320+
Some(6.0),
321+
None,
322+
Some(8.0),
323+
])), // y
324+
];
325+
326+
let result = nanvl(&args).expect("failed to initialize function nanvl");
327+
let floats =
328+
as_float64_array(&result).expect("failed to initialize function nanvl");
329+
330+
assert_eq!(floats.len(), 4);
331+
assert_eq!(floats.value(0), 1.0);
332+
assert_eq!(floats.value(1), 6.0);
333+
assert!(floats.is_null(2));
334+
assert_eq!(floats.value(3), 4.0);
335+
}
336+
337+
#[test]
338+
fn test_nanvl_f64_only_x_nulls() {
339+
// `x` has nulls, `y` does not:
340+
// - x null -> null (propagated from x)
341+
// - x NaN -> y
342+
// - x non-NaN -> x
343+
let args: Vec<ArrayRef> = vec![
344+
Arc::new(Float64Array::from(vec![
345+
None,
346+
Some(f64::NAN),
347+
Some(3.0),
348+
None,
349+
])), // x
350+
Arc::new(Float64Array::from(vec![5.0, 6.0, 7.0, 8.0])), // y
351+
];
352+
353+
let result = nanvl(&args).expect("failed to initialize function nanvl");
354+
let floats =
355+
as_float64_array(&result).expect("failed to initialize function nanvl");
356+
357+
assert_eq!(floats.len(), 4);
358+
assert!(floats.is_null(0));
359+
assert_eq!(floats.value(1), 6.0);
360+
assert_eq!(floats.value(2), 3.0);
361+
assert!(floats.is_null(3));
362+
}
238363
}

0 commit comments

Comments
 (0)