types: intern AccAddress.String() to gut wasted expensive recomputations (#8694)

Given that AccAddress is a []byte type, and its .String() method is
quite expensive, continuously invoking the method doesn't easily provide
a way for the result to be memoized. In memory profiles from
benchmarking OneBankSendTxPerBlock and run for a while, it showed >2GiB burnt
and SendCoins consuming a bunch of memory: >2GiB.

This change introduces a simple global cache with a map to intern
AccAddress values, so that we quickly look up the expensively computed
values. With it, the prior memory profiles are erased, and benchmarks
show improvements:

```shell
$ benchstat before.txt after.txt
name                     old time/op    new time/op    delta
OneBankSendTxPerBlock-8    1.94ms ± 9%    1.92ms ±11%   -1.34%  (p=0.011 n=58+57)

name                     old alloc/op   new alloc/op   delta
OneBankSendTxPerBlock-8     428kB ± 0%     365kB ± 0%  -14.67%  (p=0.000 n=58+54)

name                     old allocs/op  new allocs/op  delta
OneBankSendTxPerBlock-8     6.34k ± 0%     5.90k ± 0%   -7.06%  (p=0.000 n=58+57)
```

Fixes #8693

Co-authored-by: Alessio Treglia <alessio@tendermint.com>
This commit is contained in:
Emmanuel T Odeke 2021-02-25 03:01:03 -08:00 committed by GitHub
parent ba74a7c737
commit 929b62cd46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"strings"
"sync"
yaml "gopkg.in/yaml.v2"
@ -236,8 +237,28 @@ func (aa AccAddress) Bytes() []byte {
return aa
}
// AccAddress.String() is expensive and if unoptimized dominantly showed up in profiles,
// yet has no mechanisms to trivially cache the result given that AccAddress is a []byte type.
// With the string interning below, we are able to achieve zero cost allocations for string->[]byte
// because the Go compiler recognizes the special case of map[string([]byte)] when used exactly
// in that pattern. See https://github.com/cosmos/cosmos-sdk/issues/8693.
var addMu sync.Mutex
var addrStrMemoize = make(map[string]string)
// String implements the Stringer interface.
func (aa AccAddress) String() string {
func (aa AccAddress) String() (str string) {
addMu.Lock()
defer addMu.Unlock()
if str, ok := addrStrMemoize[string(aa)]; ok {
return str
}
// Otherwise cache it for later memoization.
defer func() {
addrStrMemoize[string(aa)] = str
}()
if aa.Empty() {
return ""
}