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 37775f3be4 perf(arrow-buffer): Format i256 values without num-bigint 
(#11000)
37775f3be4 is described below

commit 37775f3be4fc3dadc479d1513a610f2c5372564d
Author: Neil Conway <[email protected]>
AuthorDate: Sat Sep 5 22:17:50 2026 -0400

    perf(arrow-buffer): Format i256 values without num-bigint (#11000)
    
    # Which issue does this PR close?
    
    - N/A
    
    # Rationale for this change
    
    Display for i256 converted the value to a num-bigint value and formatted
    that, which allocates the BigInt's digit vector and runs a generic
    arbitrary-precision conversion.
    
    By formatting the value ourselves, we can do better because we don't
    need to support arbitrary precision, and we can also avoid the heap
    allocation and type conversion overhead.
    
    format_decimal benchmark (#10997), Apple M4 Max:
    
        case                              before     after   change
        decimal256 (76, 10) 38 digits    1730.13    958.33   -44.6%
        decimal256 (76, 10) 76 digits    2088.60   1760.90   -15.7%
    
    The win is bigger for small Decimal256 values because we can do those
    entirely in i128; in principle num-bigint could implement a similar
    optimization for small values, but it currently does not. Decimal256
    values larger than i128 are split into three chunks and formatted as
    i128; this is still faster than going through num-bigint.
    
    In practice, this improves the performance of writing out decimal values
    as CSV and JSON, as well as pretty-printing them.
    
    # What changes are included in this PR?
    
    See above.
    
    # Are these changes tested?
    
    Yes; new test added, existing tests pass.
    
    # Are there any user-facing changes?
    
    No; decimal format is unchanged.
    
    # AI usage
    
    Developed with Claude Code Fable 5.1; reviewed with Codex Astra 6. I
    reviewed, revised, and understand the resulting code.
    
    Co-authored-by: Jeffrey Vo <[email protected]>
---
 arrow-buffer/src/bigint/mod.rs | 54 +++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 53 insertions(+), 1 deletion(-)

diff --git a/arrow-buffer/src/bigint/mod.rs b/arrow-buffer/src/bigint/mod.rs
index 2a359df2c1..eb1fad0abb 100644
--- a/arrow-buffer/src/bigint/mod.rs
+++ b/arrow-buffer/src/bigint/mod.rs
@@ -72,7 +72,24 @@ impl std::fmt::Debug for i256 {
 
 impl std::fmt::Display for i256 {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", BigInt::from_signed_bytes_le(&self.to_le_bytes()))
+        if let Some(v) = Self::to_i128(*self) {
+            return write!(f, "{v}");
+        }
+
+        // The magnitude has up to 77 digits, so it splits into at most three
+        // chunks of 38 digits that each fit an i128. It is taken as unsigned
+        // limbs, which also holds the magnitude of i256::MIN.
+        let chunk = Self::from_i128(10_i128.pow(38)).as_digits();
+        let (high, low) = div_rem(&self.wrapping_abs().as_digits(), &chunk);
+        let (top, mid) = div_rem(&high, &chunk);
+        let [top, mid, low] = [top, mid, low].map(|digits| 
Self::from_digits(digits).as_i128());
+
+        let sign = if self.is_negative() { "-" } else { "" };
+        if top != 0 {
+            write!(f, "{sign}{top}{mid:038}{low:038}")
+        } else {
+            write!(f, "{sign}{mid}{low:038}")
+        }
     }
 }
 
@@ -1740,6 +1757,41 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_display_matches_bigint() {
+        let ten_pow_38 = i256::from_i128(10_i128.pow(38));
+        let mut cases = vec![
+            i256::ZERO,
+            i256::ONE,
+            i256::MINUS_ONE,
+            i256::from_i128(i128::MAX),
+            i256::from_i128(i128::MIN),
+            i256::from_i128(i128::MAX).wrapping_add(i256::ONE),
+            i256::from_i128(i128::MIN).wrapping_sub(i256::ONE),
+            ten_pow_38,
+            ten_pow_38.wrapping_sub(i256::ONE),
+            ten_pow_38.wrapping_neg(),
+            ten_pow_38.wrapping_mul(ten_pow_38),
+            ten_pow_38.wrapping_mul(ten_pow_38).wrapping_neg(),
+            ten_pow_38.wrapping_mul(ten_pow_38).wrapping_add(i256::ONE),
+            i256::MAX,
+            i256::MIN,
+            i256::MIN.wrapping_add(i256::ONE),
+        ];
+        // Every digit count, with and without zeros in the lower chunks
+        let mut value = i256::ONE;
+        while value != i256::ZERO {
+            cases.push(value);
+            cases.push(value.wrapping_sub(i256::ONE));
+            cases.push(value.wrapping_neg());
+            value = value.wrapping_mul(i256::from_i128(10));
+        }
+        for case in cases {
+            let expected = 
BigInt::from_signed_bytes_le(&case.to_le_bytes()).to_string();
+            assert_eq!(case.to_string(), expected);
+        }
+    }
+
     #[test]
     fn test_from_string() {
         let cases = [

Reply via email to