From a5bf213a50cdc0ce7e4fa8fe65a9af864589a170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Thu, 28 Mar 2024 19:15:46 +0000 Subject: [PATCH] Optimize NumDigits Dividing BitLen by math.Log2(10) is what math/big does underneath Not including the Int64/Uint64 check makes this slightly slower than old method Included 2 benchmarks, for 10 digit numbers & 100 digit numbers: -- before > go test -bench=NumDigit -run=NumDigit goos: linux goarch: amd64 pkg: github.com/shopspring/decimal cpu: AMD Ryzen 7 7840U w/ Radeon 780M Graphics BenchmarkDecimal_NumDigits10-16 18317293 63.87 ns/op BenchmarkDecimal_NumDigits100-16 3645015 329.6 ns/op -- after ... BenchmarkDecimal_NumDigits10-16 143781325 8.488 ns/op BenchmarkDecimal_NumDigits100-16 5931247 207.4 ns/op --- decimal.go | 33 +++++++++++++++++++++++++++------ decimal_bench_test.go | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/decimal.go b/decimal.go index eed845f7..089992f6 100644 --- a/decimal.go +++ b/decimal.go @@ -946,14 +946,35 @@ func (d Decimal) Ln(precision int32) (Decimal, error) { } // NumDigits returns the number of digits of the decimal coefficient (d.Value) -// Note: Current implementation is extremely slow for large decimals and/or decimals with large fractional part func (d Decimal) NumDigits() int { - d.ensureInitialized() - // Note(mwoss): It can be optimized, unnecessary cast of big.Int to string - if d.IsNegative() { - return len(d.value.String()) - 1 + if d.value == nil { + return 1 + } + + if d.value.IsUint64() { + u64 := d.value.Uint64() + if u64 < (1 << 53) { + if u64 == 0 { + return 1 + } + return int(math.Log10(float64(u64))) + 1 + } + } else if d.value.IsInt64() { + i64 := d.value.Int64() + if i64 > -(1 << 53) { + return int(math.Log10(float64(-i64))) + 1 + } } - return len(d.value.String()) + + abs := new(big.Int).Abs(d.value) + // lg10 may be off by 1, need to verify + lg10 := int(float64(abs.BitLen())/math.Log2(10)) + 1 + check := big.NewInt(int64(lg10)) + check.Exp(tenInt, check, nil) + if abs.Mul(abs, tenInt).Cmp(check) == -1 { + return lg10 - 1 + } + return lg10 } // IsInteger returns true when decimal can be represented as an integer value, otherwise, it returns false. diff --git a/decimal_bench_test.go b/decimal_bench_test.go index 269a9f61..fdbee248 100644 --- a/decimal_bench_test.go +++ b/decimal_bench_test.go @@ -120,6 +120,34 @@ func BenchmarkDecimal_RoundCash_Five(b *testing.B) { } } +func numDigits(b *testing.B, want int, val Decimal) { + b.Helper() + for i := 0; i < b.N; i++ { + if have := val.NumDigits(); have != want { + b.Fatalf("\nHave: %q\nWant: %q", have, want) + } + } +} + +func BenchmarkDecimal_NumDigits10(b *testing.B) { + numDigits(b, 10, New(3478512345, -3)) +} + +func BenchmarkDecimal_NumDigits100(b *testing.B) { + s := make([]byte, 102) + for i := range s { + s[i] = byte('0' + i%10) + } + s[0] = '-' + s[100] = '.' + d, err := NewFromString(string(s)) + if err != nil { + b.Log(d) + b.Error(err) + } + numDigits(b, 100, d) +} + func Benchmark_Cmp(b *testing.B) { decimals := DecimalSlice([]Decimal{}) for i := 0; i < 1000000; i++ { @@ -131,7 +159,7 @@ func Benchmark_Cmp(b *testing.B) { } } -func Benchmark_decimal_Decimal_Add_different_precision(b *testing.B) { +func BenchmarkDecimal_Add_different_precision(b *testing.B) { d1 := NewFromFloat(1000.123) d2 := NewFromFloat(500).Mul(NewFromFloat(0.12)) @@ -142,7 +170,7 @@ func Benchmark_decimal_Decimal_Add_different_precision(b *testing.B) { } } -func Benchmark_decimal_Decimal_Sub_different_precision(b *testing.B) { +func BenchmarkDecimal_Sub_different_precision(b *testing.B) { d1 := NewFromFloat(1000.123) d2 := NewFromFloat(500).Mul(NewFromFloat(0.12)) @@ -153,7 +181,7 @@ func Benchmark_decimal_Decimal_Sub_different_precision(b *testing.B) { } } -func Benchmark_decimal_Decimal_Add_same_precision(b *testing.B) { +func BenchmarkDecimal_Add_same_precision(b *testing.B) { d1 := NewFromFloat(1000.123) d2 := NewFromFloat(500.123) @@ -164,7 +192,7 @@ func Benchmark_decimal_Decimal_Add_same_precision(b *testing.B) { } } -func Benchmark_decimal_Decimal_Sub_same_precision(b *testing.B) { +func BenchmarkDecimal_Sub_same_precision(b *testing.B) { d1 := NewFromFloat(1000.123) d2 := NewFromFloat(500.123)