Skip to content

Commit b2dffed

Browse files
authored
convertVarIntToBytes: use reusable bytes array (#352)
Noticed while auditing and profiling dependencies of cosmos-sdk, that convertVarIntToBytes, while reusing already implemented code, it was expensively creating a bytes.Buffer (40B on 64-bit architectures) returning a result and discarding it, yet that code was called 3 times successively at least. By reusing a byte array (not a slice, to ensure bounds checks eliminations by the compiler), we are able to dramatically improve performance, taking it from ~4µs down to 850ns (~4.5X reduction), reduce allocations by >=~80% in every dimension: ```shell $ benchstat before.txt after.txt name old time/op new time/op delta ConvertLeafOp-8 3.90µs ± 1% 0.85µs ± 4% -78.12% (p=0.000 n=10+10) name old alloc/op new alloc/op delta ConvertLeafOp-8 5.18kB ± 0% 0.77kB ± 0% -85.19% (p=0.000 n=10+10) name old allocs/op new allocs/op delta ConvertLeafOp-8 120 ± 0% 24 ± 0% -80.00% (p=0.000 n=10+10) ``` Fixes #344
1 parent 9e510e5 commit b2dffed

File tree

2 files changed

+47
-15
lines changed

2 files changed

+47
-15
lines changed

encoding_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,37 @@ func TestDecodeBytes_invalidVarint(t *testing.T) {
8686
_, _, err := decodeBytes([]byte{0xff})
8787
require.Error(t, err)
8888
}
89+
90+
// sink is kept as a global to ensure that value checks and assignments to it can't be
91+
// optimized away, and this will help us ensure that benchmarks successfully run.
92+
var sink interface{}
93+
94+
func BenchmarkConvertLeafOp(b *testing.B) {
95+
var versions = []int64{
96+
0,
97+
1,
98+
100,
99+
127,
100+
128,
101+
1 << 29,
102+
-0,
103+
-1,
104+
-100,
105+
-127,
106+
-128,
107+
-1 << 29,
108+
}
109+
110+
b.ReportAllocs()
111+
b.ResetTimer()
112+
113+
for i := 0; i < b.N; i++ {
114+
for _, version := range versions {
115+
sink = convertLeafOp(version)
116+
}
117+
}
118+
if sink == nil {
119+
b.Fatal("Benchmark wasn't run")
120+
}
121+
sink = nil
122+
}

proof_ics23.go

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package iavl
22

33
import (
4-
"bytes"
4+
"encoding/binary"
55
"fmt"
66

77
ics23 "github.com/confio/ics23/go"
@@ -94,10 +94,11 @@ func convertExistenceProof(p *RangeProof, key, value []byte) (*ics23.ExistencePr
9494
}
9595

9696
func convertLeafOp(version int64) *ics23.LeafOp {
97+
var varintBuf [binary.MaxVarintLen64]byte
9798
// this is adapted from iavl/proof.go:proofLeafNode.Hash()
98-
prefix := convertVarIntToBytes(0)
99-
prefix = append(prefix, convertVarIntToBytes(1)...)
100-
prefix = append(prefix, convertVarIntToBytes(version)...)
99+
prefix := convertVarIntToBytes(0, varintBuf)
100+
prefix = append(prefix, convertVarIntToBytes(1, varintBuf)...)
101+
prefix = append(prefix, convertVarIntToBytes(version, varintBuf)...)
101102

102103
return &ics23.LeafOp{
103104
Hash: ics23.HashOp_SHA256,
@@ -114,13 +115,15 @@ func convertInnerOps(path PathToLeaf) []*ics23.InnerOp {
114115
// lengthByte is the length prefix prepended to each of the sha256 sub-hashes
115116
var lengthByte byte = 0x20
116117

118+
var varintBuf [binary.MaxVarintLen64]byte
119+
117120
// we need to go in reverse order, iavl starts from root to leaf,
118121
// we want to go up from the leaf to the root
119122
for i := len(path) - 1; i >= 0; i-- {
120123
// this is adapted from iavl/proof.go:proofInnerNode.Hash()
121-
prefix := convertVarIntToBytes(int64(path[i].Height))
122-
prefix = append(prefix, convertVarIntToBytes(path[i].Size)...)
123-
prefix = append(prefix, convertVarIntToBytes(path[i].Version)...)
124+
prefix := convertVarIntToBytes(int64(path[i].Height), varintBuf)
125+
prefix = append(prefix, convertVarIntToBytes(path[i].Size, varintBuf)...)
126+
prefix = append(prefix, convertVarIntToBytes(path[i].Version, varintBuf)...)
124127

125128
var suffix []byte
126129
if len(path[i].Left) > 0 {
@@ -147,12 +150,7 @@ func convertInnerOps(path PathToLeaf) []*ics23.InnerOp {
147150
return steps
148151
}
149152

150-
func convertVarIntToBytes(orig int64) []byte {
151-
buf := new(bytes.Buffer)
152-
err := encodeVarint(buf, orig)
153-
// write should not fail
154-
if err != nil {
155-
panic(err)
156-
}
157-
return buf.Bytes()
153+
func convertVarIntToBytes(orig int64, buf [binary.MaxVarintLen64]byte) []byte {
154+
n := binary.PutVarint(buf[:], orig)
155+
return buf[:n]
158156
}

0 commit comments

Comments
 (0)