perf: optimize spark_floor (up to 4x faster)#4911
Conversation
spark_floor (up to 4x faster)
…datafusion-comet-20260713-114953 # Conflicts: # native/spark-expr/Cargo.toml
|
Both this and #4926 edit the shared Suggested change: land one mechanism and apply it to both. The |
Adopts impl Fn on both helpers so they are symmetric and no callsite needs to pass &f, addressing coordination feedback with apache#4926 which uses impl Fn too. Floor's scalar path drops its stray &f.
|
Coordinated with #4926 per your feedback: this PR keeps |
mbutrovich
left a comment
There was a problem hiding this comment.
Thanks @andygrove. The mechanism is correct: the dispatch_pow10! macro caps EXP at 18 so 10_i64.pow(EXP) cannot overflow, the i64 fast-path result always fits back in i64 so the as i128 is exact, and scale >= 19 falls back to the runtime i128 path. The committed benches/floor.rs maps to the reported numbers and wide_decimal_array drives the cold path. The comments explain why the const EXP folds the divisor to a multiply-and-shift and why the wide case is kept out of line, which is what I want to see.
| /// [`decimal_floor_pow10`] instantiation. | ||
| #[cold] | ||
| #[inline(never)] | ||
| fn decimal_floor_wide(x: i128, div: i128) -> i128 { |
There was a problem hiding this comment.
decimal_floor_wide and the Err(_) arm of decimal_floor_pow10 are never reached by a test. test_floor_decimal128_array uses unscaled values that all fit in i64, so a regression that broke only the wide path or the negative-remainder rounding would pass CI. The benchmark exercises the cold path but asserts nothing, so it does not guard correctness.
The ceil sibling (#4926) closed this exact gap with test_ceil_decimal128_wide_array. Please add the floor analogue so the shared path is covered on the base PR:
#[test]
fn test_floor_decimal128_wide_array() -> Result<()> {
// Unscaled values above i64::MAX (~9.22e18) take the wide fallback in decimal_floor_pow10.
// Scale 6 targeting Decimal128(38, 0):
// 20_000_000_000_000_000_000 / 10^6 = 20_000_000_000_000 (exact multiple)
// 20_000_000_000_000_000_001 / 10^6 -> floor 20_000_000_000_000 (toward zero)
// -20_000_000_000_000_000_001 / 10^6 -> floor -20_000_000_000_001 (toward -inf)
let array = Decimal128Array::from(vec![
Some(20_000_000_000_000_000_000_i128),
Some(20_000_000_000_000_000_001_i128),
Some(-20_000_000_000_000_000_001_i128),
None,
])
.with_precision_and_scale(38, 6)?;
let args = vec![ColumnarValue::Array(Arc::new(array))];
let ColumnarValue::Array(result) = spark_floor(&args, &DataType::Decimal128(38, 0))? else {
unreachable!()
};
let expected = Decimal128Array::from(vec![
Some(20_000_000_000_000_i128),
Some(20_000_000_000_000_i128),
Some(-20_000_000_000_001_i128),
None,
])
.with_precision_and_scale(38, 0)?;
let actual = result.as_any().downcast_ref::<Decimal128Array>().unwrap();
assert_eq!(actual, &expected);
Ok(())
}The negative row is the one worth pinning, since floor rounds toward negative infinity and that arm is otherwise untested.
Which issue does this PR close?
N/A
Rationale for this change
Optimize an existing expression.
What changes are included in this PR?
Decimal floor now monomorphizes the per-element op (was &dyn Fn) and dispatches on input scale to a const divisor with an i64 fast path, so the per-row 128-bit __divti3 division libcall becomes a multiply-and-shift.
How are these changes tested?
Existing tests.
Benchmark (criterion):
Full criterion output: