perf: math: optimize and test FormatInt + simplify LegacyNewDecFromStr (#14010)

Optimizes and tests FormatInt by removing inefficient string
concatenation but also making it so much clearer with how one would
express adding thousand separators in natural language. It uses
a combination of strings.Builder whose values can be grown

The performance improvement is stark in every dimension:

```shell
$ benchstat before.txt after3.txt
name                          old time/op    new time/op    delta
DecimalValueRendererFormat-8    4.48µs ± 1%    2.11µs ± 2%  -52.90%  (p=0.000 n=10+10)

name                          old alloc/op   new alloc/op   delta
DecimalValueRendererFormat-8    3.62kB ± 0%    0.78kB ± 0%  -78.59%  (p=0.000 n=10+10)

name                          old allocs/op  new allocs/op  delta
DecimalValueRendererFormat-8      83.0 ± 0%      28.0 ± 0%  -66.27%  (p=0.000 n=10+10)
```

While here, also simplified zero padding for LegacyNewDecFromStr
simply by using strings.Repeat instead of a convoluted
fmt.Sprintf+strconv.Itoa.

Fixes #14008
Fixes #14003

Co-authored-by: Marko <marbar3778@yahoo.com>
This commit is contained in:
Emmanuel T Odeke 2022-11-29 13:00:47 -08:00 committed by GitHub
parent 8c6181aacf
commit 3e85182baa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 71 additions and 6 deletions

View File

@ -31,5 +31,6 @@ Ref: https://keepachangelog.com/en/1.0.0/
## [Unreleased]
* [#14010](https://github.com/cosmos/cosmos-sdk/pull/14010) Optimize FormatInt to not do plain string concentation when formatting thousands and instead build it more efficiently.
* [#13381](https://github.com/cosmos/cosmos-sdk/pull/13381) Add uint `IsNil` method.
* [#12634](https://github.com/cosmos/cosmos-sdk/pull/12634) Move `sdk.Dec` to math package, call it `LegacyDec`.

View File

@ -178,7 +178,7 @@ func LegacyNewDecFromStr(str string) (LegacyDec, error) {
// add some extra zero's to correct to the Precision factor
zerosToAdd := LegacyPrecision - lenDecs
zeros := fmt.Sprintf(`%0`+strconv.Itoa(zerosToAdd)+`s`, "")
zeros := strings.Repeat("0", zerosToAdd)
combinedStr += zeros
combined, ok := new(big.Int).SetString(combinedStr, 10) // base 10

View File

@ -6,6 +6,7 @@ import (
"fmt"
"math/big"
"strings"
"sync"
"testing"
)
@ -448,6 +449,10 @@ func hasOnlyDigits(s string) bool {
const thousandSeparator string = "'"
var stringsBuilderPool = &sync.Pool{
New: func() any { return new(strings.Builder) },
}
// FormatInt formats an integer (encoded as in protobuf) into a value-rendered
// string following ADR-050. This function operates with string manipulation
// (instead of manipulating the int or sdk.Int object).
@ -466,11 +471,34 @@ func FormatInt(v string) (string, error) {
return "", fmt.Errorf("expecting only digits 0-9, but got non-digits in %q", v)
}
startOffset := 3
for outputIndex := len(v); outputIndex > startOffset; {
outputIndex -= 3
v = v[:outputIndex] + thousandSeparator + v[outputIndex:]
// 1. Less than 4 digits don't need any formatting.
if len(v) <= 3 {
return sign + v, nil
}
return sign + v, nil
sb := stringsBuilderPool.Get().(*strings.Builder)
defer stringsBuilderPool.Put(sb)
sb.Reset()
sb.Grow(len(v) + len(v)/3) // Exactly v + numberOfThousandSeparatorsIn(v)
// 2. If the length of v is not a multiple of 3 e.g. 1234 or 12345, to achieve 1'234 or 12'345,
// we can simply slide to the first mod3 values of v that aren't the multiples of 3 then insert in
// the thousands separator so in this case: write(12'); then the remaining v will be entirely multiple
// of 3 hence v = 34*
if mod3 := len(v) % 3; mod3 != 0 {
sb.WriteString(v[:mod3])
v = v[mod3:]
sb.WriteString(thousandSeparator)
}
// 3. By this point v is entirely multiples of 3 hence we just insert the separator at every 3 digit.
for i := 0; i < len(v); i += 3 {
end := i + 3
sb.WriteString(v[i:end])
if end < len(v) {
sb.WriteString(thousandSeparator)
}
}
return sign + sb.String(), nil
}

View File

@ -472,3 +472,39 @@ func TestFormatIntNonDigits(t *testing.T) {
})
}
}
func TestFormatIntCorrectness(t *testing.T) {
tests := []struct {
in string
want string
}{
{"0", "0"},
{"-2", "-2"},
{"10", "10"},
{"123", "123"},
{"1234", "1'234"},
{"12345", "12'345"},
{"123456", "123'456"},
{"-123456", "-123'456"},
{"1234567", "1'234'567"},
{"12345678", "12'345'678"},
{"123456789", "123'456'789"},
{"12345678910", "12'345'678'910"},
{"9999999999999999", "9'999'999'999'999'999"},
{"-9999999999999999", "-9'999'999'999'999'999"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.in, func(t *testing.T) {
got, err := math.FormatInt(tt.in)
if err != nil {
t.Fatal(err)
}
if got != tt.want {
t.Fatalf("Mismatch:\n\tGot: %q\n\tWant: %q", got, tt.want)
}
})
}
}