From a90b021785ecb653172140ad1879771120ebc022 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Tue, 23 May 2023 19:07:49 +0300 Subject: [PATCH 1/2] perf: math: make Int.Size() faster by computation not len(MarshalledBytes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change computes Int.Size() by checking bit lengths and translating those to base 10 values. This is instead of firstly invoking .Marshal to check the length, yet .MarshalTo requires invoking .Size() then .Marshal which is double work. The results show improvements: ```shell $ benchstat before.txt after.txt name old time/op new time/op delta IntSize-8 23.9µs ± 3% 20.3µs ± 1% -15.15% (p=0.000 n=9+9) name old alloc/op new alloc/op delta IntSize-8 6.62kB ± 0% 5.09kB ± 0% -23.19% (p=0.000 n=10+10) name old allocs/op new allocs/op delta IntSize-8 186 ± 0% 177 ± 0% -4.84% (p=0.000 n=10+10) ``` Fixes #10331 --- math/CHANGELOG.md | 2 ++ math/int.go | 85 +++++++++++++++++++++++++++++++++++++++++++++-- math/int_test.go | 58 ++++++++++++++++++++++++++++++++ 3 files changed, 142 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..d83b3544e145 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,87 @@ 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) +var 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 += 1 + // 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 +} From 81eba1e4baa63ae75a1f1f230a6ec0855947d652 Mon Sep 17 00:00:00 2001 From: marbar3778 Date: Thu, 8 Jun 2023 12:02:55 +0200 Subject: [PATCH 2/2] lint fix --- math/int.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/math/int.go b/math/int.go index d83b3544e145..d8b08f8eb7b3 100644 --- a/math/int.go +++ b/math/int.go @@ -429,8 +429,10 @@ func (i *Int) Unmarshal(data []byte) error { // 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) -var log10Of2 = stdmath.Log10(2) +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() @@ -447,7 +449,7 @@ func (i *Int) Size() (size int) { // on comparisons inside sizeBigInt, hence we make a copy // of ii and make it absolute having taken note of the sign // already. - size += 1 + 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)