Skip to content

Commit

Permalink
perf: math: make Int.Size() faster by computation not len(MarshalledB…
Browse files Browse the repository at this point in the history
…ytes) (cosmos#16263)

Co-authored-by: marbar3778 <[email protected]>
  • Loading branch information
odeke-em and tac0turtle authored Jun 8, 2023
1 parent 5235593 commit 9b9e319
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 3 deletions.
2 changes: 2 additions & 0 deletions math/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
87 changes: 84 additions & 3 deletions math/int.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding"
"encoding/json"
"fmt"
stdmath "math"
"math/big"
"strings"
"sync"
Expand Down Expand Up @@ -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.
Expand Down
58 changes: 58 additions & 0 deletions math/int_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit 9b9e319

Please sign in to comment.