From 9b9e319d1abc8f07e85011df4c427b6518949027 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Thu, 8 Jun 2023 08:15:52 -0700 Subject: [PATCH] perf: math: make Int.Size() faster by computation not len(MarshalledBytes) (#16263) Co-authored-by: marbar3778 --- math/CHANGELOG.md | 2 ++ math/int.go | 87 +++++++++++++++++++++++++++++++++++++++++++++-- math/int_test.go | 58 +++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 3 deletions(-) diff --git a/math/CHANGELOG.md b/math/CHANGELOG.md index b337d786d431..ee89f6e414ca 100644 --- a/math/CHANGELOG.md +++ b/math/CHANGELOG.md @@ -44,6 +44,8 @@ Ref: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.j ### Improvements +* [#16263](https://github.com/cosmos/cosmos-sdk/pull/16263) Improved math/Int.Size by computing the decimal digits count instead of firstly invoking .Marshal() then checking the length + * [#15768](https://github.com/cosmos/cosmos-sdk/pull/15768) Removed the second call to the `init` method for the global variable `grand`. * [#16141](https://github.com/cosmos/cosmos-sdk/pull/16141) Speedup `LegacyDec.ApproxRoot` and `LegacyDec.ApproxSqrt`. diff --git a/math/int.go b/math/int.go index 3e6b5c257dec..d8b08f8eb7b3 100644 --- a/math/int.go +++ b/math/int.go @@ -4,6 +4,7 @@ import ( "encoding" "encoding/json" "fmt" + stdmath "math" "math/big" "strings" "sync" @@ -421,9 +422,89 @@ func (i *Int) Unmarshal(data []byte) error { } // Size implements the gogo proto custom type interface. -func (i *Int) Size() int { - bz, _ := i.Marshal() - return len(bz) +// Reduction power of 10 is the smallest power of 10, than 1<<64-1 +// +// 18446744073709551615 +// +// and the next value fitting with the digits of (1<<64)-1 is: +// +// 10000000000000000000 +var ( + big10Pow19, _ = new(big.Int).SetString("1"+strings.Repeat("0", 19), 10) + log10Of2 = stdmath.Log10(2) +) + +func (i *Int) Size() (size int) { + sign := i.Sign() + if sign == 0 { // It is zero. + // log*(0) is undefined hence return early. + return 1 + } + + ii := i.i + alreadyMadeCopy := false + if sign < 0 { // Negative sign encountered, so consider len("-") + // The reason that we make this comparison in here is to + // allow checking for negatives exactly once, to reduce + // on comparisons inside sizeBigInt, hence we make a copy + // of ii and make it absolute having taken note of the sign + // already. + size++ + // We already accounted for the negative sign above, thus + // we can now compute the length of the absolute value. + ii = new(big.Int).Abs(ii) + alreadyMadeCopy = true + } + + // From here on, we are now dealing with non-0, non-negative values. + return size + sizeBigInt(ii, alreadyMadeCopy) +} + +func sizeBigInt(i *big.Int, alreadyMadeCopy bool) (size int) { + // This code assumes that non-0, non-negative values have been passed in. + bitLen := i.BitLen() + + res := float64(bitLen) * log10Of2 + ires := int(res) + if diff := res - float64(ires); diff == 0.0 { + return size + ires + } else if diff >= 0.3 { // There are other digits past the bitLen, this is a heuristic. + return size + ires + 1 + } + + // Use Log10(x) for values less than (1<<64)-1, given it is only defined for [1, (1<<64)-1] + if bitLen <= 64 { + return size + 1 + int(stdmath.Log10(float64(i.Uint64()))) + } + // Past this point, the value is greater than (1<<64)-1 and 10^19. + + // The prior above computation of i.BitLen() * log10Of2 is inaccurate for powers of 10 + // and values like "9999999999999999999999999999"; that computation always overshoots by 1 + // hence our next alternative is to just go old school and keep dividing the value by: + // 10^19 aka "10000000000000000000" while incrementing size += 19 + + // At this point we should just keep reducing by 10^19 as that's the smallest multiple + // of 10 that matches the digit length of (1<<64)-1 + var ri *big.Int + if alreadyMadeCopy { + ri = i + } else { + ri = new(big.Int).Set(i) + alreadyMadeCopy = true + } + + for ri.Cmp(big10Pow19) >= 0 { // Keep reducing the value by 10^19 and increment size by 19 + ri = ri.Quo(ri, big10Pow19) + size += 19 + } + + if ri.Sign() == 0 { // if the value is zero, no need for the recursion, just return immediately + return size + } + + // Otherwise we already know how many times we reduced the value, so its + // remnants less than 10^19 and those can be computed by again calling sizeBigInt. + return size + sizeBigInt(ri, alreadyMadeCopy) } // Override Amino binary serialization by proxying to protobuf. diff --git a/math/int_test.go b/math/int_test.go index 56b05cc69f94..d9f54da11520 100644 --- a/math/int_test.go +++ b/math/int_test.go @@ -513,3 +513,61 @@ func TestFormatIntCorrectness(t *testing.T) { }) } } + +var sizeTests = []struct { + s string + want int +}{ + {"0", 1}, + {"-0", 1}, + {"-10", 3}, + {"-10000", 6}, + {"10000", 5}, + {"100000", 6}, + {"99999", 5}, + {"10000000000", 11}, + {"18446744073709551616", 20}, + {"18446744073709551618", 20}, + {"184467440737095516181", 21}, + {"100000000000000000000000", 24}, + {"1000000000000000000000000000", 28}, + {"9000000000099999999999999999", 28}, + {"9999999999999999999999999999", 28}, + {"9903520314283042199192993792", 28}, + {"340282366920938463463374607431768211456", 39}, + {"3402823669209384634633746074317682114569999", 43}, + {"9999999999999999999999999999999999999999999", 43}, + {"99999999999999999999999999999999999999999999", 44}, + {"999999999999999999999999999999999999999999999", 45}, + {"90000000000999999999999999999000000000099999999999999999", 56}, + {"-90000000000999999999999999999000000000099999999999999999", 57}, + {"9000000000099999999999999999900000000009999999999999999990", 58}, + {"990000000009999999999999999990000000000999999999999999999999", 60}, + {"99000000000999999999999999999000000000099999999999999999999919", 62}, + {"90000000000999999990000000000000000000000000000000000000000000", 62}, + {"99999999999999999999999999990000000000000000000000000000000000", 62}, + {"11111111111111119999999999990000000000000000000000000000000000", 62}, + {"99000000000999999999999999999000000000099999999999999999999919", 62}, + {"10000000000000000000000000000000000000000000000000000000000000", 62}, + {"10000000000000000000000000000000000000000000000000000000000000000000000000000", 77}, + {"99999999999999999999999999999999999999999999999999999999999999999999999999999", 77}, + {"110000000000000000000000000000000000000000000000000000000000000000000000000009", 78}, +} + +func BenchmarkIntSize(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + for _, st := range sizeTests { + ii, _ := math.NewIntFromString(st.s) + got := ii.Size() + if got != st.want { + b.Errorf("%q:: got=%d, want=%d", st.s, got, st.want) + } + sink = got + } + } + if sink == nil { + b.Fatal("Benchmark did not run!") + } + sink = nil +}