Remove unnecessary decimal rescaling - memory usage optimization (#160)

* Remove unnecessary decimal rescaling

* Move benchmarks to separate file, add new benchmark tests
This commit is contained in:
Mateusz Woś 2020-01-06 00:12:15 +01:00 committed by GitHub
parent 96defcb63c
commit 408a2507e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 214 additions and 143 deletions

View file

@ -390,6 +390,14 @@ func NewFromFloatWithExponent(value float64, exp int32) Decimal {
//
func (d Decimal) rescale(exp int32) Decimal {
d.ensureInitialized()
if d.exp == exp {
return Decimal{
new(big.Int).Set(d.value),
d.exp,
}
}
// NOTE(vadim): must convert exps to float64 before - to prevent overflow
diff := math.Abs(float64(exp) - float64(d.exp))
value := new(big.Int).Set(d.value)
@ -419,27 +427,23 @@ func (d Decimal) Abs() Decimal {
// Add returns d + d2.
func (d Decimal) Add(d2 Decimal) Decimal {
baseScale := min(d.exp, d2.exp)
rd := d.rescale(baseScale)
rd2 := d2.rescale(baseScale)
rd, rd2 := RescalePair(d, d2)
d3Value := new(big.Int).Add(rd.value, rd2.value)
return Decimal{
value: d3Value,
exp: baseScale,
exp: rd.exp,
}
}
// Sub returns d - d2.
func (d Decimal) Sub(d2 Decimal) Decimal {
baseScale := min(d.exp, d2.exp)
rd := d.rescale(baseScale)
rd2 := d2.rescale(baseScale)
rd, rd2 := RescalePair(d, d2)
d3Value := new(big.Int).Sub(rd.value, rd2.value)
return Decimal{
value: d3Value,
exp: baseScale,
exp: rd.exp,
}
}
@ -600,9 +604,7 @@ func (d Decimal) Cmp(d2 Decimal) int {
return d.value.Cmp(d2.value)
}
baseExp := min(d.exp, d2.exp)
rd := d.rescale(baseExp)
rd2 := d2.rescale(baseExp)
rd, rd2 := RescalePair(d, d2)
return rd.value.Cmp(rd2.value)
}
@ -1172,6 +1174,22 @@ func Avg(first Decimal, rest ...Decimal) Decimal {
return sum.Div(count)
}
// Rescale two decimals to common exponential value (minimal exp of both decimals)
func RescalePair(d1 Decimal, d2 Decimal) (Decimal, Decimal) {
d1.ensureInitialized()
d2.ensureInitialized()
if d1.exp == d2.exp {
return d1, d2
}
baseScale := min(d1.exp, d2.exp)
if baseScale != d1.exp {
return d1.rescale(baseScale), d2
}
return d1, d2.rescale(baseScale)
}
func min(x, y int32) int32 {
if x >= y {
return y

185
decimal_bench_test.go Normal file
View file

@ -0,0 +1,185 @@
package decimal
import (
"math"
"math/rand"
"sort"
"strconv"
"testing"
)
type DecimalSlice []Decimal
func (p DecimalSlice) Len() int { return len(p) }
func (p DecimalSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p DecimalSlice) Less(i, j int) bool { return p[i].Cmp(p[j]) < 0 }
func BenchmarkNewFromFloatWithExponent(b *testing.B) {
rng := rand.New(rand.NewSource(0xdead1337))
in := make([]float64, b.N)
for i := range in {
in[i] = rng.NormFloat64() * 10e20
}
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
in := rng.NormFloat64() * 10e20
_ = NewFromFloatWithExponent(in, math.MinInt32)
}
}
func BenchmarkNewFromFloat(b *testing.B) {
rng := rand.New(rand.NewSource(0xdead1337))
in := make([]float64, b.N)
for i := range in {
in[i] = rng.NormFloat64() * 10e20
}
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
_ = NewFromFloat(in[i])
}
}
func BenchmarkNewFromStringFloat(b *testing.B) {
rng := rand.New(rand.NewSource(0xdead1337))
in := make([]float64, b.N)
for i := range in {
in[i] = rng.NormFloat64() * 10e20
}
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
in := strconv.FormatFloat(in[i], 'f', -1, 64)
_, _ = NewFromString(in)
}
}
func Benchmark_FloorFast(b *testing.B) {
input := New(200, 2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
input.Floor()
}
}
func Benchmark_FloorRegular(b *testing.B) {
input := New(200, -2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
input.Floor()
}
}
func Benchmark_DivideOriginal(b *testing.B) {
tcs := createDivTestCases()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, tc := range tcs {
d := tc.d
if sign(tc.d2) == 0 {
continue
}
d2 := tc.d2
prec := tc.prec
a := d.DivOld(d2, int(prec))
if sign(a) > 2 {
panic("dummy panic")
}
}
}
}
func Benchmark_DivideNew(b *testing.B) {
tcs := createDivTestCases()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, tc := range tcs {
d := tc.d
if sign(tc.d2) == 0 {
continue
}
d2 := tc.d2
prec := tc.prec
a := d.DivRound(d2, prec)
if sign(a) > 2 {
panic("dummy panic")
}
}
}
}
func BenchmarkDecimal_RoundCash_Five(b *testing.B) {
const want = "3.50"
for i := 0; i < b.N; i++ {
val := New(3478, -3)
if have := val.StringFixedCash(5); have != want {
b.Fatalf("\nHave: %q\nWant: %q", have, want)
}
}
}
func BenchmarkDecimal_RoundCash_Fifteen(b *testing.B) {
const want = "6.30"
for i := 0; i < b.N; i++ {
val := New(635, -2)
if have := val.StringFixedCash(15); have != want {
b.Fatalf("\nHave: %q\nWant: %q", have, want)
}
}
}
func Benchmark_Cmp(b *testing.B) {
decimals := DecimalSlice([]Decimal{})
for i := 0; i < 1000000; i++ {
decimals = append(decimals, New(int64(i), 0))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
sort.Sort(decimals)
}
}
func Benchmark_decimal_Decimal_Add_different_precision(b *testing.B) {
d1 := NewFromFloat(1000.123)
d2 := NewFromFloat(500).Mul(NewFromFloat(0.12))
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
d1.Add(d2)
}
}
func Benchmark_decimal_Decimal_Sub_different_precision(b *testing.B) {
d1 := NewFromFloat(1000.123)
d2 := NewFromFloat(500).Mul(NewFromFloat(0.12))
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
d1.Sub(d2)
}
}
func Benchmark_decimal_Decimal_Add_same_precision(b *testing.B) {
d1 := NewFromFloat(1000.123)
d2 := NewFromFloat(500.123)
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
d1.Add(d2)
}
}
func Benchmark_decimal_Decimal_Sub_same_precision(b *testing.B) {
d1 := NewFromFloat(1000.123)
d2 := NewFromFloat(500.123)
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
d1.Add(d2)
}
}

View file

@ -9,7 +9,6 @@ import (
"math/big"
"math/rand"
"reflect"
"sort"
"strconv"
"strings"
"testing"
@ -201,47 +200,6 @@ func TestNewFromFloat32Quick(t *testing.T) {
}
}
func BenchmarkNewFromFloatWithExponent(b *testing.B) {
rng := rand.New(rand.NewSource(0xdead1337))
in := make([]float64, b.N)
for i := range in {
in[i] = rng.NormFloat64() * 10e20
}
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
in := rng.NormFloat64() * 10e20
_ = NewFromFloatWithExponent(in, math.MinInt32)
}
}
func BenchmarkNewFromFloat(b *testing.B) {
rng := rand.New(rand.NewSource(0xdead1337))
in := make([]float64, b.N)
for i := range in {
in[i] = rng.NormFloat64() * 10e20
}
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
_ = NewFromFloat(in[i])
}
}
func BenchmarkNewFromStringFloat(b *testing.B) {
rng := rand.New(rand.NewSource(0xdead1337))
in := make([]float64, b.N)
for i := range in {
in[i] = rng.NormFloat64() * 10e20
}
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
in := strconv.FormatFloat(in[i], 'f', -1, 64)
_, _ = NewFromString(in)
}
}
func TestNewFromString(t *testing.T) {
for _, x := range testTable {
s := x.short
@ -862,22 +820,6 @@ func TestDecimal_Floor(t *testing.T) {
}
}
func Benchmark_FloorFast(b *testing.B) {
input := New(200, 2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
input.Floor()
}
}
func Benchmark_FloorRegular(b *testing.B) {
input := New(200, -2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
input.Floor()
}
}
func TestDecimal_Ceil(t *testing.T) {
assertCeil := func(input, expected Decimal) {
got := input.Ceil()
@ -1525,44 +1467,6 @@ func (d Decimal) DivOld(d2 Decimal, prec int) Decimal {
return ret
}
func Benchmark_DivideOriginal(b *testing.B) {
tcs := createDivTestCases()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, tc := range tcs {
d := tc.d
if sign(tc.d2) == 0 {
continue
}
d2 := tc.d2
prec := tc.prec
a := d.DivOld(d2, int(prec))
if sign(a) > 2 {
panic("dummy panic")
}
}
}
}
func Benchmark_DivideNew(b *testing.B) {
tcs := createDivTestCases()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, tc := range tcs {
d := tc.d
if sign(tc.d2) == 0 {
continue
}
d2 := tc.d2
prec := tc.prec
a := d.DivRound(d2, prec)
if sign(a) > 2 {
panic("dummy panic")
}
}
}
}
func sign(d Decimal) int {
return d.value.Sign()
}
@ -1719,26 +1623,6 @@ func TestDecimal_RoundCash_Panic(t *testing.T) {
d.RoundCash(231)
}
func BenchmarkDecimal_RoundCash_Five(b *testing.B) {
const want = "3.50"
for i := 0; i < b.N; i++ {
val := New(3478, -3)
if have := val.StringFixedCash(5); have != want {
b.Fatalf("\nHave: %q\nWant: %q", have, want)
}
}
}
func BenchmarkDecimal_RoundCash_Fifteen(b *testing.B) {
const want = "6.30"
for i := 0; i < b.N; i++ {
val := New(635, -2)
if have := val.StringFixedCash(15); have != want {
b.Fatalf("\nHave: %q\nWant: %q", have, want)
}
}
}
func TestDecimal_Mod(t *testing.T) {
type Inp struct {
a string
@ -2193,22 +2077,6 @@ func TestDecimal_Coefficient(t *testing.T) {
}
}
type DecimalSlice []Decimal
func (p DecimalSlice) Len() int { return len(p) }
func (p DecimalSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p DecimalSlice) Less(i, j int) bool { return p[i].Cmp(p[j]) < 0 }
func Benchmark_Cmp(b *testing.B) {
decimals := DecimalSlice([]Decimal{})
for i := 0; i < 1000000; i++ {
decimals = append(decimals, New(int64(i), 0))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
sort.Sort(decimals)
}
}
func TestNullDecimal_Scan(t *testing.T) {
// test the Scan method that implements the
// sql.Scanner interface