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

Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 2f03e2262b perf(arrow-cast): Scan integer and fractional decimal 
digits in separate loops (#10974)
2f03e2262b is described below

commit 2f03e2262ba233587a26be93c6d23a21b6b57a53
Author: Neil Conway <[email protected]>
AuthorDate: Fri Sep 4 07:10:38 2026 -0400

    perf(arrow-cast): Scan integer and fractional decimal digits in separate 
loops (#10974)
    
    # Which issue does this PR close?
    
    - Closes #10961.
    
    # Rationale for this change
    
    We recently consolidated the decimal parsing code to use a single, more
    correct implementation (#10850); however, this resulted in regression
    performance somewhat.
    
    `parse_decimal` used a single loop to scan each digit of the input.
    Digits before and after the decimal point are treated very differently;
    profiling the code revealed that doing a data-dependent branch for each
    digit resulted in worse codegen.
    
    Instead, we can split the parsing logic to use two loops, in sequence:
    first look for digits that precede the decimal point, then those that
    follow. This avoids the data-dependent branch and seems to significantly
    improve codegen.
    
    On M4 Max, this improves the end-to-end CSV parsing benchmark by 5-15%,
    and the `parse_decimal` microbenchmark by 5-33%.
    
    # What changes are included in this PR?
    
    * Split decimal parsing logic into two sequential loops
    * Refactor digit accumulation logic to avoid duplication
    * Mark `fold_decimal_chunk` as `inline(always)` -- the refactor resulted
    in LLVM deciding not to inline this call inside the hot loop, which
    regressed performance significantly.
    
    # Are these changes tested?
    
    Yes, covered by existing tests.
    
    # Are there any user-facing changes?
    
    No.
    
    # Tool usage
    
    Explored optimization ideas and developed this optimization with Claude
    Code (Fable 5.1). I revised and understand the resulting code.
---
 arrow-cast/src/parse.rs | 107 +++++++++++++++++++++++++++++++++---------------
 1 file changed, 74 insertions(+), 33 deletions(-)

diff --git a/arrow-cast/src/parse.rs b/arrow-cast/src/parse.rs
index 81b4b5949e..d077a9c5a4 100644
--- a/arrow-cast/src/parse.rs
+++ b/arrow-cast/src/parse.rs
@@ -962,54 +962,60 @@ fn parse_decimal_mantissa<T: DecimalType>(
         }
     };
 
-    let mut value = T::Native::ZERO;
-    let mut chunk = 0_u64;
-    let mut chunk_len = 0_usize;
-    let mut saw_point = false;
+    let mut acc = DecimalAccumulator::<T> {
+        value: T::Native::ZERO,
+        chunk: 0,
+        chunk_len: 0,
+        negative,
+    };
     let mut int_kept = 0_usize;
     let mut frac_kept = 0_usize;
     let mut first_discarded_digit = None;
 
+    // Digits before the decimal point
     let mut index = 0;
     while let Some(&b) = mantissa.get(index) {
-        match b {
-            b'0'..=b'9' => {
-                let digit = b - b'0';
-                let (kept, keep) = if saw_point {
-                    (&mut frac_kept, frac_keep)
-                } else {
-                    (&mut int_kept, int_keep)
-                };
-                if *kept < keep {
-                    *kept += 1;
-                    // Cannot overflow: the chunk is folded into `value` 
before it
-                    // exceeds MAX_CHUNK_DIGITS digits, all of which fit in a 
u64
-                    chunk = chunk * 10 + digit as u64;
-                    chunk_len += 1;
-                    if chunk_len == MAX_CHUNK_DIGITS {
-                        value = fold_decimal_chunk::<T>(value, chunk, 
chunk_len, negative)?;
-                        chunk = 0;
-                        chunk_len = 0;
-                    }
-                } else {
-                    first_discarded_digit.get_or_insert(digit);
-                }
-            }
-            b'.' if !saw_point => saw_point = true,
-            b'e' | b'E' => return Err(MantissaError::Exponent(index)),
-            _ => return Err(MantissaError::InvalidFormat),
+        if !b.is_ascii_digit() {
+            break;
+        }
+        if int_kept < int_keep {
+            int_kept += 1;
+            acc.push(b - b'0')?;
+        } else {
+            first_discarded_digit.get_or_insert(b - b'0');
         }
         index += 1;
     }
 
-    if chunk_len > 0 {
-        value = fold_decimal_chunk::<T>(value, chunk, chunk_len, negative)?;
+    // Digits after the decimal point
+    if mantissa.get(index) == Some(&b'.') {
+        index += 1;
+        while let Some(&b) = mantissa.get(index) {
+            if !b.is_ascii_digit() {
+                break;
+            }
+            if frac_kept < frac_keep {
+                frac_kept += 1;
+                acc.push(b - b'0')?;
+            } else {
+                first_discarded_digit.get_or_insert(b - b'0');
+            }
+            index += 1;
+        }
+    }
+
+    match mantissa.get(index) {
+        None => {}
+        Some(b'e' | b'E') => return Err(MantissaError::Exponent(index)),
+        Some(_) => return Err(MantissaError::InvalidFormat),
     }
 
     if int_kept == 0 && frac_kept == 0 && first_discarded_digit.is_none() {
         return Err(MantissaError::InvalidFormat);
     }
 
+    let mut value = acc.finish()?;
+
     // Scale the value up to the target scale. Skipped for zero, where 
computing
     // 10^missing could overflow the native type even though the result (zero)
     // is always representable.
@@ -1062,10 +1068,45 @@ fn split_sign(bytes: &[u8]) -> (bool, &[u8]) {
     }
 }
 
+/// Accumulates decimal digits into `chunk`, folding it into `value` whenever
+/// it reaches [`MAX_CHUNK_DIGITS`] digits
+struct DecimalAccumulator<T: DecimalType> {
+    value: T::Native,
+    chunk: u64,
+    chunk_len: usize,
+    negative: bool,
+}
+
+impl<T: DecimalType> DecimalAccumulator<T> {
+    #[inline(always)]
+    fn push(&mut self, digit: u8) -> Result<(), DecimalParseError> {
+        // Cannot overflow: the chunk is folded into `value` before it exceeds
+        // MAX_CHUNK_DIGITS digits, all of which fit in a u64
+        self.chunk = self.chunk * 10 + digit as u64;
+        self.chunk_len += 1;
+        if self.chunk_len == MAX_CHUNK_DIGITS {
+            self.value =
+                fold_decimal_chunk::<T>(self.value, self.chunk, 
self.chunk_len, self.negative)?;
+            self.chunk = 0;
+            self.chunk_len = 0;
+        }
+        Ok(())
+    }
+
+    /// Folds the digits still in `chunk` into the value
+    #[inline]
+    fn finish(self) -> Result<T::Native, DecimalParseError> {
+        if self.chunk_len == 0 {
+            return Ok(self.value);
+        }
+        fold_decimal_chunk::<T>(self.value, self.chunk, self.chunk_len, 
self.negative)
+    }
+}
+
 /// Folds a chunk of up to [`MAX_CHUNK_DIGITS`] digits into `value`, producing
 /// `value * 10^chunk_len + chunk` (`chunk` is negated first when parsing a
 /// negative number).
-#[inline]
+#[inline(always)]
 fn fold_decimal_chunk<T: DecimalType>(
     value: T::Native,
     chunk: u64,

Reply via email to