From afe2a660417e2a7f70ce64cf52611ae7d06ea2ea 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 1/5] 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 | 29 +++++++++++++++++++++++------ decimal_bench_test.go | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/decimal.go b/decimal.go index 24a8aa3..247c09b 100644 --- a/decimal.go +++ b/decimal.go @@ -1224,14 +1224,31 @@ 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)) + check := big.NewInt(int64(lg10)) + return lg10 + abs.Cmp(check.Exp(tenInt, check, nil)) } // 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 b1978bc..24659dc 100644 --- a/decimal_bench_test.go +++ b/decimal_bench_test.go @@ -121,6 +121,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++ { @@ -132,7 +160,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)) @@ -143,7 +171,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)) @@ -154,7 +182,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) @@ -165,7 +193,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) From 0a79029c95a3382ab626ed767a704670e57f6a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Wed, 3 Apr 2024 20:29:23 +0000 Subject: [PATCH 2/5] feedback, fixes bug --- decimal.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/decimal.go b/decimal.go index 247c09b..84b92f1 100644 --- a/decimal.go +++ b/decimal.go @@ -1245,10 +1245,17 @@ func (d Decimal) NumDigits() int { } abs := new(big.Int).Abs(d.value) - // lg10 may be off by 1, need to verify - lg10 := int(float64(abs.BitLen()) / math.Log2(10)) - check := big.NewInt(int64(lg10)) - return lg10 + abs.Cmp(check.Exp(tenInt, check, nil)) + estimatedNumDigits := int(float64(abs.BitLen()) / math.Log2(10)) + + // estimatedNumDigits (lg10) may be off by 1, need to verify + digitsBigInt := big.NewInt(int64(estimatedNumDigits)) + errorCorrectionUnit := digitsBigInt.Exp(tenInt, digitsBigInt, nil) + + if abs.Cmp(errorCorrectionUnit) >= 0 { + return estimatedNumDigits + 1 + } + + return estimatedNumDigits } // IsInteger returns true when decimal can be represented as an integer value, otherwise, it returns false. From d538aec685f133c814cedca428ddbb97f134c4d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Wed, 3 Apr 2024 21:09:19 +0000 Subject: [PATCH 3/5] remove copy, don't need Abs(d.value) 9007199254740992 converts to float64 --- decimal.go | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/decimal.go b/decimal.go index 84b92f1..e95825d 100644 --- a/decimal.go +++ b/decimal.go @@ -1229,29 +1229,23 @@ func (d Decimal) NumDigits() int { return 1 } - if d.value.IsUint64() { - u64 := d.value.Uint64() - if u64 < (1 << 53) { - if u64 == 0 { + if d.value.IsInt64() { + i64 := d.value.Int64() + if i64 <= (1<<53) && i64 >= -(1<<53) { + if i64 == 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 int(math.Log10(math.Abs(float64(i64)))) + 1 } } - abs := new(big.Int).Abs(d.value) - estimatedNumDigits := int(float64(abs.BitLen()) / math.Log2(10)) + estimatedNumDigits := int(float64(d.value.BitLen()) / math.Log2(10)) // estimatedNumDigits (lg10) may be off by 1, need to verify digitsBigInt := big.NewInt(int64(estimatedNumDigits)) errorCorrectionUnit := digitsBigInt.Exp(tenInt, digitsBigInt, nil) - if abs.Cmp(errorCorrectionUnit) >= 0 { + if d.value.CmpAbs(errorCorrectionUnit) >= 0 { return estimatedNumDigits + 1 } From 445700e706f8a17dff4e20e10e5020e2ef8b797f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Thu, 4 Apr 2024 13:33:36 +0000 Subject: [PATCH 4/5] %d --- decimal_bench_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decimal_bench_test.go b/decimal_bench_test.go index 24659dc..34e038f 100644 --- a/decimal_bench_test.go +++ b/decimal_bench_test.go @@ -125,7 +125,7 @@ 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) + b.Fatalf("\nHave: %d\nWant: %d", have, want) } } } From 20bfa00fcdc5b304cae08d1f5d859d972681694b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Thu, 4 Apr 2024 13:38:23 +0000 Subject: [PATCH 5/5] comment on significance of 53 bit significand --- decimal.go | 1 + 1 file changed, 1 insertion(+) diff --git a/decimal.go b/decimal.go index e95825d..ef2bacb 100644 --- a/decimal.go +++ b/decimal.go @@ -1231,6 +1231,7 @@ func (d Decimal) NumDigits() int { if d.value.IsInt64() { i64 := d.value.Int64() + // restrict fast path to integers with exact conversion to float64 if i64 <= (1<<53) && i64 >= -(1<<53) { if i64 == 0 { return 1