Skip to content

Commit 595e614

Browse files
authored
perf: optimize trunc for scalar precision case (10x faster) (#23593)
## Which issue does this PR close? N/A ## Rationale for this change Optimize existing expression. ## What changes are included in this PR? Added an array fast path for trunc(value, scalar precision) that hoists 10^precision out of the per-element loop and uses a unary kernel instead of broadcasting the precision and recomputing powi per element. ## Are these changes tested? Existing tests. Benchmark (criterion): - trunc f32 precision array_ 1024: 87.531% faster (base 1284ns -> cand 160ns) - trunc f64 precision array_ 1024: 81.29% faster (base 1313ns -> cand 245ns) - trunc f32 precision array_ 4096: 91.021% faster (base 4542ns -> cand 407ns) - trunc f64 precision array_ 4096: 82.983% faster (base 4560ns -> cand 776ns) - trunc f32 precision array_ 8192: 92.235% faster (base 9475ns -> cand 735ns) - trunc f64 precision array_ 8192: 81.919% faster (base 10319ns -> cand 1865ns) Full criterion output: ```text trunc f64 precision array: 1024 time: [245.17 ns 245.40 ns 245.70 ns] change: [−81.321% −81.290% −81.259%] (p = 0.00 < 0.05) Performance has improved. Found 3 outliers among 100 measurements (3.00%) 2 (2.00%) high mild 1 (1.00%) high severe trunc f32 precision array: 1024 time: [159.81 ns 160.13 ns 160.65 ns] change: [−87.558% −87.531% −87.495%] (p = 0.00 < 0.05) Performance has improved. Found 15 outliers among 100 measurements (15.00%) 6 (6.00%) high mild 9 (9.00%) high severe trunc f64 precision array: 4096 time: [764.34 ns 768.58 ns 773.05 ns] change: [−83.091% −82.983% −82.845%] (p = 0.00 < 0.05) Performance has improved. Found 8 outliers among 100 measurements (8.00%) 2 (2.00%) high mild 6 (6.00%) high severe trunc f32 precision array: 4096 time: [404.97 ns 405.35 ns 405.93 ns] change: [−91.049% −91.021% −90.991%] (p = 0.00 < 0.05) Performance has improved. Found 13 outliers among 100 measurements (13.00%) 2 (2.00%) high mild 11 (11.00%) high severe trunc f64 precision array: 8192 time: [1.8676 µs 1.8741 µs 1.8813 µs] change: [−81.960% −81.919% −81.874%] (p = 0.00 < 0.05) Performance has improved. Found 1 outliers among 100 measurements (1.00%) 1 (1.00%) high mild trunc f32 precision array: 8192 time: [729.32 ns 731.77 ns 734.41 ns] change: [−92.264% −92.235% −92.203%] (p = 0.00 < 0.05) Performance has improved. Found 6 outliers among 100 measurements (6.00%) 6 (6.00%) high mild ``` ## Are there any user-facing changes? <!-- If there are user-facing changes then we may require documentation to be updated before approving the PR. --> <!-- If there are any breaking changes to public APIs, please add the `api change` label. -->
1 parent 12fe9a0 commit 595e614

3 files changed

Lines changed: 149 additions & 7 deletions

File tree

datafusion/functions/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,11 @@ harness = false
301301
name = "trunc"
302302
required-features = ["math_expressions"]
303303

304+
[[bench]]
305+
harness = false
306+
name = "trunc_precision"
307+
required-features = ["math_expressions"]
308+
304309
[[bench]]
305310
harness = false
306311
name = "initcap"
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
//! Benchmarks the `trunc(value, precision)` array path where `precision` is a
19+
//! constant (scalar) argument.
20+
21+
use arrow::datatypes::{DataType, Field, Float32Type, Float64Type};
22+
use arrow::util::bench_util::create_primitive_array;
23+
use criterion::{Criterion, criterion_group, criterion_main};
24+
use datafusion_common::ScalarValue;
25+
use datafusion_common::config::ConfigOptions;
26+
use datafusion_expr::{ColumnarValue, ScalarFunctionArgs};
27+
use datafusion_functions::math::trunc;
28+
use std::hint::black_box;
29+
use std::sync::Arc;
30+
31+
fn criterion_benchmark(c: &mut Criterion) {
32+
let trunc = trunc();
33+
let config_options = Arc::new(ConfigOptions::default());
34+
35+
for size in [1024, 4096, 8192] {
36+
let f64_array = Arc::new(create_primitive_array::<Float64Type>(size, 0.2));
37+
let f64_args = vec![
38+
ColumnarValue::Array(f64_array),
39+
ColumnarValue::Scalar(ScalarValue::Int64(Some(3))),
40+
];
41+
let arg_fields = vec![
42+
Field::new("a", DataType::Float64, true).into(),
43+
Field::new("p", DataType::Int64, false).into(),
44+
];
45+
let return_field = Field::new("f", DataType::Float64, true).into();
46+
c.bench_function(&format!("trunc f64 precision array: {size}"), |b| {
47+
b.iter(|| {
48+
black_box(
49+
trunc
50+
.invoke_with_args(ScalarFunctionArgs {
51+
args: f64_args.clone(),
52+
arg_fields: arg_fields.clone(),
53+
number_rows: size,
54+
return_field: Arc::clone(&return_field),
55+
config_options: Arc::clone(&config_options),
56+
})
57+
.unwrap(),
58+
)
59+
})
60+
});
61+
62+
let f32_array = Arc::new(create_primitive_array::<Float32Type>(size, 0.2));
63+
let f32_args = vec![
64+
ColumnarValue::Array(f32_array),
65+
ColumnarValue::Scalar(ScalarValue::Int64(Some(3))),
66+
];
67+
let arg_fields = vec![
68+
Field::new("a", DataType::Float32, true).into(),
69+
Field::new("p", DataType::Int64, false).into(),
70+
];
71+
let return_field = Field::new("f", DataType::Float32, true).into();
72+
c.bench_function(&format!("trunc f32 precision array: {size}"), |b| {
73+
b.iter(|| {
74+
black_box(
75+
trunc
76+
.invoke_with_args(ScalarFunctionArgs {
77+
args: f32_args.clone(),
78+
arg_fields: arg_fields.clone(),
79+
number_rows: size,
80+
return_field: Arc::clone(&return_field),
81+
config_options: Arc::clone(&config_options),
82+
})
83+
.unwrap(),
84+
)
85+
})
86+
});
87+
}
88+
}
89+
90+
criterion_group!(benches, criterion_benchmark);
91+
criterion_main!(benches);

datafusion/functions/src/math/trunc.rs

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ use arrow::datatypes::DataType::{
2525
Decimal32, Decimal64, Decimal128, Decimal256, Float32, Float64,
2626
};
2727
use arrow::datatypes::{
28-
DataType, Decimal32Type, Decimal64Type, Decimal128Type, Decimal256Type, DecimalType,
29-
Float32Type, Float64Type, Int64Type,
28+
ArrowPrimitiveType, DataType, Decimal32Type, Decimal64Type, Decimal128Type,
29+
Decimal256Type, DecimalType, Float32Type, Float64Type, Int64Type,
3030
};
3131
use datafusion_common::ScalarValue::Int64;
3232
use datafusion_common::types::{
@@ -40,7 +40,7 @@ use datafusion_expr::{
4040
};
4141
use datafusion_expr_common::signature::{Coercion, TypeSignature, TypeSignatureClass};
4242
use datafusion_macros::user_doc;
43-
use num_traits::{One, Zero, pow};
43+
use num_traits::{Float, NumCast, One, Zero, pow};
4444

4545
#[user_doc(
4646
doc_section(label = "Math Functions"),
@@ -158,6 +158,12 @@ impl ScalarUDFImpl for TruncFunc {
158158
}
159159
};
160160

161+
// Whether an explicit precision argument was supplied. The array fast
162+
// paths below must only apply to the two-argument form: single-argument
163+
// `trunc(x)` uses a different zero handling (mapping `-0.0` to `0.0`)
164+
// that must be preserved.
165+
let has_precision_arg = args.args.len() == 2;
166+
161167
// Scalar fast path using tuple matching for (value, precision)
162168
match (&args.args[0], precision) {
163169
// Null cases
@@ -231,6 +237,25 @@ impl ScalarUDFImpl for TruncFunc {
231237
*lscale,
232238
))),
233239

240+
// Array value with a constant (scalar) precision: hoist the power
241+
// of ten out of the per-element loop instead of broadcasting the
242+
// scalar into a full precision array and recomputing `10^p` for
243+
// every element (see `truncate_float_array`).
244+
(ColumnarValue::Array(arr), Some(p))
245+
if has_precision_arg && arr.data_type() == &Float64 =>
246+
{
247+
Ok(ColumnarValue::Array(truncate_float_array::<Float64Type>(
248+
arr, p,
249+
)))
250+
}
251+
(ColumnarValue::Array(arr), Some(p))
252+
if has_precision_arg && arr.data_type() == &Float32 =>
253+
{
254+
Ok(ColumnarValue::Array(truncate_float_array::<Float32Type>(
255+
arr, p,
256+
)))
257+
}
258+
234259
// Array path for everything else
235260
_ => make_scalar_function(trunc, vec![])(&args.args),
236261
}
@@ -375,14 +400,35 @@ fn trunc(args: &[ArrayRef]) -> Result<ArrayRef> {
375400
}
376401
}
377402

378-
fn compute_truncate32(x: f32, y: i64) -> f32 {
379-
let factor = 10.0_f32.powi(y as i32);
403+
/// Truncates `x` using a pre-computed `factor` of `10^precision`. Taking the
404+
/// factor as an argument lets callers hoist `10^precision` out of a per-element
405+
/// loop when the precision is constant.
406+
fn truncate_with_factor<F: Float>(x: F, factor: F) -> F {
380407
(x * factor).trunc() / factor
381408
}
382409

410+
/// Truncates every element of a float array to `precision` decimal places,
411+
/// computing the `10^precision` factor once and reusing it for every element.
412+
fn truncate_float_array<T>(arr: &ArrayRef, precision: i64) -> ArrayRef
413+
where
414+
T: ArrowPrimitiveType,
415+
T::Native: Float,
416+
{
417+
let factor = <T::Native as NumCast>::from(10.0_f64)
418+
.unwrap()
419+
.powi(precision as i32);
420+
Arc::new(
421+
arr.as_primitive::<T>()
422+
.unary::<_, T>(|x| truncate_with_factor(x, factor)),
423+
)
424+
}
425+
426+
fn compute_truncate32(x: f32, y: i64) -> f32 {
427+
truncate_with_factor(x, 10.0_f32.powi(y as i32))
428+
}
429+
383430
fn compute_truncate64(x: f64, y: i64) -> f64 {
384-
let factor = 10.0_f64.powi(y as i32);
385-
(x * factor).trunc() / factor
431+
truncate_with_factor(x, 10.0_f64.powi(y as i32))
386432
}
387433

388434
/// Truncates a decimal value to `truncate_precision` fractional digits.

0 commit comments

Comments
 (0)