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:
parent
8c6181aacf
commit
3e85182baa
@ -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`.
|
||||
|
||||
@ -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
|
||||
|
||||
38
math/int.go
38
math/int.go
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user