Skip to content

Commit a1b8892

Browse files
authored
trie: improve node rlp decoding performance (#25357)
This avoids copying the input []byte while decoding trie nodes. In most cases, particularly when the input slice is provided by the underlying database, this optimization is safe to use. For cases where the origin of the input slice is unclear, the copying version is retained. The new code performs better even when the input must be copied, because it is now only copied once in decodeNode.
1 parent cce7f08 commit a1b8892

File tree

3 files changed

+158
-7
lines changed

3 files changed

+158
-7
lines changed

trie/database.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,10 @@ func (n *cachedNode) rlp() []byte {
163163
// or by regenerating it from the rlp encoded blob.
164164
func (n *cachedNode) obj(hash common.Hash) node {
165165
if node, ok := n.node.(rawNode); ok {
166-
return mustDecodeNode(hash[:], node)
166+
// The raw-blob format nodes are loaded from either from
167+
// clean cache or the database, they are all in their own
168+
// copy and safe to use unsafe decoder.
169+
return mustDecodeNodeUnsafe(hash[:], node)
167170
}
168171
return expandNode(hash[:], n.node)
169172
}
@@ -346,7 +349,10 @@ func (db *Database) node(hash common.Hash) node {
346349
if enc := db.cleans.Get(nil, hash[:]); enc != nil {
347350
memcacheCleanHitMeter.Mark(1)
348351
memcacheCleanReadMeter.Mark(int64(len(enc)))
349-
return mustDecodeNode(hash[:], enc)
352+
353+
// The returned value from cache is in its own copy,
354+
// safe to use mustDecodeNodeUnsafe for decoding.
355+
return mustDecodeNodeUnsafe(hash[:], enc)
350356
}
351357
}
352358
// Retrieve the node from the dirty cache if available
@@ -371,7 +377,9 @@ func (db *Database) node(hash common.Hash) node {
371377
memcacheCleanMissMeter.Mark(1)
372378
memcacheCleanWriteMeter.Mark(int64(len(enc)))
373379
}
374-
return mustDecodeNode(hash[:], enc)
380+
// The returned value from database is in its own copy,
381+
// safe to use mustDecodeNodeUnsafe for decoding.
382+
return mustDecodeNodeUnsafe(hash[:], enc)
375383
}
376384

377385
// Node retrieves an encoded cached trie node from memory. If it cannot be found

trie/node.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ func (n valueNode) fstring(ind string) string {
9999
return fmt.Sprintf("%x ", []byte(n))
100100
}
101101

102+
// mustDecodeNode is a wrapper of decodeNode and panic if any error is encountered.
102103
func mustDecodeNode(hash, buf []byte) node {
103104
n, err := decodeNode(hash, buf)
104105
if err != nil {
@@ -107,8 +108,29 @@ func mustDecodeNode(hash, buf []byte) node {
107108
return n
108109
}
109110

110-
// decodeNode parses the RLP encoding of a trie node.
111+
// mustDecodeNodeUnsafe is a wrapper of decodeNodeUnsafe and panic if any error is
112+
// encountered.
113+
func mustDecodeNodeUnsafe(hash, buf []byte) node {
114+
n, err := decodeNodeUnsafe(hash, buf)
115+
if err != nil {
116+
panic(fmt.Sprintf("node %x: %v", hash, err))
117+
}
118+
return n
119+
}
120+
121+
// decodeNode parses the RLP encoding of a trie node. It will deep-copy the passed
122+
// byte slice for decoding, so it's safe to modify the byte slice afterwards. The-
123+
// decode performance of this function is not optimal, but it is suitable for most
124+
// scenarios with low performance requirements and hard to determine whether the
125+
// byte slice be modified or not.
111126
func decodeNode(hash, buf []byte) (node, error) {
127+
return decodeNodeUnsafe(hash, common.CopyBytes(buf))
128+
}
129+
130+
// decodeNodeUnsafe parses the RLP encoding of a trie node. The passed byte slice
131+
// will be directly referenced by node without bytes deep copy, so the input MUST
132+
// not be changed after.
133+
func decodeNodeUnsafe(hash, buf []byte) (node, error) {
112134
if len(buf) == 0 {
113135
return nil, io.ErrUnexpectedEOF
114136
}
@@ -141,7 +163,7 @@ func decodeShort(hash, elems []byte) (node, error) {
141163
if err != nil {
142164
return nil, fmt.Errorf("invalid value node: %v", err)
143165
}
144-
return &shortNode{key, append(valueNode{}, val...), flag}, nil
166+
return &shortNode{key, valueNode(val), flag}, nil
145167
}
146168
r, _, err := decodeRef(rest)
147169
if err != nil {
@@ -164,7 +186,7 @@ func decodeFull(hash, elems []byte) (*fullNode, error) {
164186
return n, err
165187
}
166188
if len(val) > 0 {
167-
n.Children[16] = append(valueNode{}, val...)
189+
n.Children[16] = valueNode(val)
168190
}
169191
return n, nil
170192
}
@@ -190,7 +212,7 @@ func decodeRef(buf []byte) (node, []byte, error) {
190212
// empty node
191213
return nil, rest, nil
192214
case kind == rlp.String && len(val) == 32:
193-
return append(hashNode{}, val...), rest, nil
215+
return hashNode(val), rest, nil
194216
default:
195217
return nil, nil, fmt.Errorf("invalid RLP string size %d (want 0 or 32)", len(val))
196218
}

trie/node_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"bytes"
2121
"testing"
2222

23+
"github.com/ethereum/go-ethereum/crypto"
2324
"github.com/ethereum/go-ethereum/rlp"
2425
)
2526

@@ -92,3 +93,123 @@ func TestDecodeFullNode(t *testing.T) {
9293
t.Fatalf("decode full node err: %v", err)
9394
}
9495
}
96+
97+
// goos: darwin
98+
// goarch: arm64
99+
// pkg: github.com/ethereum/go-ethereum/trie
100+
// BenchmarkEncodeShortNode
101+
// BenchmarkEncodeShortNode-8 16878850 70.81 ns/op 48 B/op 1 allocs/op
102+
func BenchmarkEncodeShortNode(b *testing.B) {
103+
node := &shortNode{
104+
Key: []byte{0x1, 0x2},
105+
Val: hashNode(randBytes(32)),
106+
}
107+
b.ResetTimer()
108+
b.ReportAllocs()
109+
110+
for i := 0; i < b.N; i++ {
111+
nodeToBytes(node)
112+
}
113+
}
114+
115+
// goos: darwin
116+
// goarch: arm64
117+
// pkg: github.com/ethereum/go-ethereum/trie
118+
// BenchmarkEncodeFullNode
119+
// BenchmarkEncodeFullNode-8 4323273 284.4 ns/op 576 B/op 1 allocs/op
120+
func BenchmarkEncodeFullNode(b *testing.B) {
121+
node := &fullNode{}
122+
for i := 0; i < 16; i++ {
123+
node.Children[i] = hashNode(randBytes(32))
124+
}
125+
b.ResetTimer()
126+
b.ReportAllocs()
127+
128+
for i := 0; i < b.N; i++ {
129+
nodeToBytes(node)
130+
}
131+
}
132+
133+
// goos: darwin
134+
// goarch: arm64
135+
// pkg: github.com/ethereum/go-ethereum/trie
136+
// BenchmarkDecodeShortNode
137+
// BenchmarkDecodeShortNode-8 7925638 151.0 ns/op 157 B/op 4 allocs/op
138+
func BenchmarkDecodeShortNode(b *testing.B) {
139+
node := &shortNode{
140+
Key: []byte{0x1, 0x2},
141+
Val: hashNode(randBytes(32)),
142+
}
143+
blob := nodeToBytes(node)
144+
hash := crypto.Keccak256(blob)
145+
146+
b.ResetTimer()
147+
b.ReportAllocs()
148+
149+
for i := 0; i < b.N; i++ {
150+
mustDecodeNode(hash, blob)
151+
}
152+
}
153+
154+
// goos: darwin
155+
// goarch: arm64
156+
// pkg: github.com/ethereum/go-ethereum/trie
157+
// BenchmarkDecodeShortNodeUnsafe
158+
// BenchmarkDecodeShortNodeUnsafe-8 9027476 128.6 ns/op 109 B/op 3 allocs/op
159+
func BenchmarkDecodeShortNodeUnsafe(b *testing.B) {
160+
node := &shortNode{
161+
Key: []byte{0x1, 0x2},
162+
Val: hashNode(randBytes(32)),
163+
}
164+
blob := nodeToBytes(node)
165+
hash := crypto.Keccak256(blob)
166+
167+
b.ResetTimer()
168+
b.ReportAllocs()
169+
170+
for i := 0; i < b.N; i++ {
171+
mustDecodeNodeUnsafe(hash, blob)
172+
}
173+
}
174+
175+
// goos: darwin
176+
// goarch: arm64
177+
// pkg: github.com/ethereum/go-ethereum/trie
178+
// BenchmarkDecodeFullNode
179+
// BenchmarkDecodeFullNode-8 1597462 761.9 ns/op 1280 B/op 18 allocs/op
180+
func BenchmarkDecodeFullNode(b *testing.B) {
181+
node := &fullNode{}
182+
for i := 0; i < 16; i++ {
183+
node.Children[i] = hashNode(randBytes(32))
184+
}
185+
blob := nodeToBytes(node)
186+
hash := crypto.Keccak256(blob)
187+
188+
b.ResetTimer()
189+
b.ReportAllocs()
190+
191+
for i := 0; i < b.N; i++ {
192+
mustDecodeNode(hash, blob)
193+
}
194+
}
195+
196+
// goos: darwin
197+
// goarch: arm64
198+
// pkg: github.com/ethereum/go-ethereum/trie
199+
// BenchmarkDecodeFullNodeUnsafe
200+
// BenchmarkDecodeFullNodeUnsafe-8 1789070 687.1 ns/op 704 B/op 17 allocs/op
201+
func BenchmarkDecodeFullNodeUnsafe(b *testing.B) {
202+
node := &fullNode{}
203+
for i := 0; i < 16; i++ {
204+
node.Children[i] = hashNode(randBytes(32))
205+
}
206+
blob := nodeToBytes(node)
207+
hash := crypto.Keccak256(blob)
208+
209+
b.ResetTimer()
210+
b.ReportAllocs()
211+
212+
for i := 0; i < b.N; i++ {
213+
mustDecodeNodeUnsafe(hash, blob)
214+
}
215+
}

0 commit comments

Comments
 (0)