This is an automated email from the ASF dual-hosted git repository.

andygrove pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git


The following commit(s) were added to refs/heads/main by this push:
     new 2cba02f669 perf: optimize `spark_floor` (up to 4x faster) (#4911)
2cba02f669 is described below

commit 2cba02f6695ad57941dccd43767ac923cbc0ec26
Author: Andy Grove <[email protected]>
AuthorDate: Mon Aug 3 20:17:29 2026 -0600

    perf: optimize `spark_floor` (up to 4x faster) (#4911)
    
    * perf: optimize spark_floor in datafusion-comet-spark-expr
    
    * refactor: unify make_decimal_scalar/array signatures on impl Fn
    
    Adopts impl Fn on both helpers so they are symmetric and no callsite needs 
to pass &f, addressing coordination feedback with #4926 which uses impl Fn too. 
Floor's scalar path drops its stray &f.
    
    * test: cover the wide and large-scale decimal paths in spark_floor
    
    `decimal_floor_wide` and the `Err(_)` arm of `decimal_floor_pow10` had no 
test:
    `test_floor_decimal128_array` uses unscaled values that all fit in i64, so a
    regression confined to the wide path passed CI, and the benchmark that 
drives
    that path asserts nothing.
    
    - `test_floor_decimal128_wide_array`: unscaled values above i64::MAX, 
including
      a negative one, since floor rounds toward negative infinity and that arm 
was
      otherwise untested. Mirrors `test_ceil_decimal128_wide_array` from the 
ceil
      sibling so the shared path is covered on this PR too.
    - `test_floor_decimal128_i64_boundary_array`: straddles i64::MAX and 
i64::MIN,
      where `decimal_floor_pow10` switches between the 64-bit and 128-bit
      divisions. Each pair differs by one unscaled unit and must floor to the 
same
      value, so the two paths are asserted to agree at the seam.
    - `test_floor_decimal128_large_scale_array`: scale 19, where 
`dispatch_pow10!`
      stops specializing and falls back to the runtime `decimal_floor_f`. That
      fallback was previously only reached through the scalar path.
    
    Verified by mutation: with `decimal_floor_wide` returning `div_floor(x, 
div) + 1`,
    the two new wide-path tests fail and all nine pre-existing tests still pass.
---
 native/spark-expr/Cargo.toml              |  1 +
 native/spark-expr/src/math_funcs/floor.rs | 86 +++++++++++++++++++++++++++++++
 2 files changed, 87 insertions(+)

diff --git a/native/spark-expr/Cargo.toml b/native/spark-expr/Cargo.toml
index 5d518387f5..b85153a59d 100644
--- a/native/spark-expr/Cargo.toml
+++ b/native/spark-expr/Cargo.toml
@@ -214,3 +214,4 @@ harness = false
 [[bench]]
 name = "unscaled_value"
 harness = false
+
diff --git a/native/spark-expr/src/math_funcs/floor.rs 
b/native/spark-expr/src/math_funcs/floor.rs
index 973bbab708..f97c66e2c8 100644
--- a/native/spark-expr/src/math_funcs/floor.rs
+++ b/native/spark-expr/src/math_funcs/floor.rs
@@ -213,6 +213,92 @@ mod test {
         Ok(())
     }
 
+    #[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(())
+    }
+
+    #[test]
+    fn test_floor_decimal128_i64_boundary_array() -> Result<()> {
+        // decimal_floor_pow10 switches from the i64 fast path to 
decimal_floor_wide exactly at
+        // i64::MAX / i64::MIN. Straddling both edges pins that the two paths 
agree: each pair
+        // below differs by one unscaled unit but floors to the same value, so 
a divergence
+        // between the 64-bit and 128-bit divisions would show up as a 
mismatch here.
+        let array = Decimal128Array::from(vec![
+            Some(i64::MAX as i128),     //  9223372036854.775807 -> fast path
+            Some(i64::MAX as i128 + 1), //  9223372036854.775808 -> wide path
+            Some(i64::MIN as i128),     // -9223372036854.775808 -> fast path
+            Some(i64::MIN as i128 - 1), // -9223372036854.775809 -> wide path
+        ])
+        .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(9_223_372_036_854_i128),
+            Some(9_223_372_036_854_i128),
+            Some(-9_223_372_036_855_i128),
+            Some(-9_223_372_036_855_i128),
+        ])
+        .with_precision_and_scale(38, 0)?;
+        let actual = 
result.as_any().downcast_ref::<Decimal128Array>().unwrap();
+        assert_eq!(actual, &expected);
+        Ok(())
+    }
+
+    #[test]
+    fn test_floor_decimal128_large_scale_array() -> Result<()> {
+        // dispatch_pow10! only specializes scales whose divisor fits in an 
i64, so scale 19 takes
+        // the runtime `decimal_floor_f` fallback. Untested on the array path 
otherwise.
+        let array = Decimal128Array::from(vec![
+            Some(20_000_000_000_000_000_000_000_i128),  //  2000.0
+            Some(20_000_000_000_000_000_000_001_i128),  //  
2000.0000000000000000001
+            Some(-20_000_000_000_000_000_000_001_i128), // 
-2000.0000000000000000001
+            None,
+        ])
+        .with_precision_and_scale(38, 19)?;
+        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(2000_i128),
+            Some(2000_i128),
+            Some(-2001_i128),
+            None,
+        ])
+        .with_precision_and_scale(38, 0)?;
+        let actual = 
result.as_any().downcast_ref::<Decimal128Array>().unwrap();
+        assert_eq!(actual, &expected);
+        Ok(())
+    }
+
     #[test]
     fn test_floor_f32_scalar() -> Result<()> {
         let args = 
vec![ColumnarValue::Scalar(ScalarValue::Float32(Some(125.9345)))];


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to