Skip to content

Commit 65ed1a6

Browse files
qianbinfjl
andauthored
rlp, trie: faster trie node encoding (ethereum#24126)
This change speeds up trie hashing and all other activities that require RLP encoding of trie nodes by approximately 20%. The speedup is achieved by avoiding reflection overhead during node encoding. The interface type trie.node now contains a method 'encode' that works with rlp.EncoderBuffer. Management of EncoderBuffers is left to calling code. trie.hasher, which is pooled to avoid allocations, now maintains an EncoderBuffer. This means memory resources related to trie node encoding are tied to the hasher pool. Co-authored-by: Felix Lange <fjl@twurst.com>
1 parent d1f6a9f commit 65ed1a6

12 files changed

+286
-181
lines changed

rlp/encbuffer.go

+36-15
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,31 @@ func (buf *encBuffer) size() int {
3636
return len(buf.str) + buf.lhsize
3737
}
3838

39-
// toBytes creates the encoder output.
40-
func (w *encBuffer) toBytes() []byte {
39+
// makeBytes creates the encoder output.
40+
func (w *encBuffer) makeBytes() []byte {
4141
out := make([]byte, w.size())
42+
w.copyTo(out)
43+
return out
44+
}
45+
46+
func (w *encBuffer) copyTo(dst []byte) {
4247
strpos := 0
4348
pos := 0
4449
for _, head := range w.lheads {
4550
// write string data before header
46-
n := copy(out[pos:], w.str[strpos:head.offset])
51+
n := copy(dst[pos:], w.str[strpos:head.offset])
4752
pos += n
4853
strpos += n
4954
// write the header
50-
enc := head.encode(out[pos:])
55+
enc := head.encode(dst[pos:])
5156
pos += len(enc)
5257
}
5358
// copy string data after the last list header
54-
copy(out[pos:], w.str[strpos:])
55-
return out
59+
copy(dst[pos:], w.str[strpos:])
5660
}
5761

58-
// toWriter writes the encoder output to w.
59-
func (buf *encBuffer) toWriter(w io.Writer) (err error) {
62+
// writeTo writes the encoder output to w.
63+
func (buf *encBuffer) writeTo(w io.Writer) (err error) {
6064
strpos := 0
6165
for _, head := range buf.lheads {
6266
// write string data before header
@@ -252,6 +256,19 @@ func (r *encReader) next() []byte {
252256
}
253257
}
254258

259+
func encBufferFromWriter(w io.Writer) *encBuffer {
260+
switch w := w.(type) {
261+
case EncoderBuffer:
262+
return w.buf
263+
case *EncoderBuffer:
264+
return w.buf
265+
case *encBuffer:
266+
return w
267+
default:
268+
return nil
269+
}
270+
}
271+
255272
// EncoderBuffer is a buffer for incremental encoding.
256273
//
257274
// The zero value is NOT ready for use. To get a usable buffer,
@@ -279,14 +296,10 @@ func (w *EncoderBuffer) Reset(dst io.Writer) {
279296
// If the destination writer has an *encBuffer, use it.
280297
// Note that w.ownBuffer is left false here.
281298
if dst != nil {
282-
if outer, ok := dst.(*encBuffer); ok {
299+
if outer := encBufferFromWriter(dst); outer != nil {
283300
*w = EncoderBuffer{outer, nil, false}
284301
return
285302
}
286-
if outer, ok := dst.(EncoderBuffer); ok {
287-
*w = EncoderBuffer{outer.buf, nil, false}
288-
return
289-
}
290303
}
291304

292305
// Get a fresh buffer.
@@ -303,7 +316,7 @@ func (w *EncoderBuffer) Reset(dst io.Writer) {
303316
func (w *EncoderBuffer) Flush() error {
304317
var err error
305318
if w.dst != nil {
306-
err = w.buf.toWriter(w.dst)
319+
err = w.buf.writeTo(w.dst)
307320
}
308321
// Release the internal buffer.
309322
if w.ownBuffer {
@@ -315,7 +328,15 @@ func (w *EncoderBuffer) Flush() error {
315328

316329
// ToBytes returns the encoded bytes.
317330
func (w *EncoderBuffer) ToBytes() []byte {
318-
return w.buf.toBytes()
331+
return w.buf.makeBytes()
332+
}
333+
334+
// AppendToBytes appends the encoded bytes to dst.
335+
func (w *EncoderBuffer) AppendToBytes(dst []byte) []byte {
336+
size := w.buf.size()
337+
out := append(dst, make([]byte, size)...)
338+
w.buf.copyTo(out[len(dst):])
339+
return out
319340
}
320341

321342
// Write appends b directly to the encoder output.

rlp/encode.go

+3-7
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,16 @@ type Encoder interface {
5656
// Please see package-level documentation of encoding rules.
5757
func Encode(w io.Writer, val interface{}) error {
5858
// Optimization: reuse *encBuffer when called by EncodeRLP.
59-
if buf, ok := w.(*encBuffer); ok {
59+
if buf := encBufferFromWriter(w); buf != nil {
6060
return buf.encode(val)
6161
}
62-
if ebuf, ok := w.(EncoderBuffer); ok {
63-
return ebuf.buf.encode(val)
64-
}
6562

6663
buf := getEncBuffer()
6764
defer encBufferPool.Put(buf)
68-
6965
if err := buf.encode(val); err != nil {
7066
return err
7167
}
72-
return buf.toWriter(w)
68+
return buf.writeTo(w)
7369
}
7470

7571
// EncodeToBytes returns the RLP encoding of val.
@@ -81,7 +77,7 @@ func EncodeToBytes(val interface{}) ([]byte, error) {
8177
if err := buf.encode(val); err != nil {
8278
return nil, err
8379
}
84-
return buf.toBytes(), nil
80+
return buf.makeBytes(), nil
8581
}
8682

8783
// EncodeToReader returns a reader from which the RLP encoding of val

rlp/encode_test.go

+15
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,21 @@ func TestEncodeToBytes(t *testing.T) {
399399
runEncTests(t, EncodeToBytes)
400400
}
401401

402+
func TestEncodeAppendToBytes(t *testing.T) {
403+
buffer := make([]byte, 20)
404+
runEncTests(t, func(val interface{}) ([]byte, error) {
405+
w := NewEncoderBuffer(nil)
406+
defer w.Flush()
407+
408+
err := Encode(w, val)
409+
if err != nil {
410+
return nil, err
411+
}
412+
output := w.AppendToBytes(buffer[:0])
413+
return output, nil
414+
})
415+
}
416+
402417
func TestEncodeToReader(t *testing.T) {
403418
runEncTests(t, func(val interface{}) ([]byte, error) {
404419
_, r, err := EncodeToReader(val)

trie/committer.go

-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ type leaf struct {
4444
// By 'some level' of parallelism, it's still the case that all leaves will be
4545
// processed sequentially - onleaf will never be called in parallel or out of order.
4646
type committer struct {
47-
tmp sliceBuffer
4847
sha crypto.KeccakState
4948

5049
onleaf LeafCallback
@@ -55,7 +54,6 @@ type committer struct {
5554
var committerPool = sync.Pool{
5655
New: func() interface{} {
5756
return &committer{
58-
tmp: make(sliceBuffer, 0, 550), // cap is as large as a full fullNode.
5957
sha: sha3.NewLegacyKeccak256().(crypto.KeccakState),
6058
}
6159
},

trie/database.go

+4-15
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,9 @@ func (n rawFullNode) cache() (hashNode, bool) { panic("this should never end u
113113
func (n rawFullNode) fstring(ind string) string { panic("this should never end up in a live trie") }
114114

115115
func (n rawFullNode) EncodeRLP(w io.Writer) error {
116-
var nodes [17]node
117-
118-
for i, child := range n {
119-
if child != nil {
120-
nodes[i] = child
121-
} else {
122-
nodes[i] = nilValueNode
123-
}
124-
}
125-
return rlp.Encode(w, nodes)
116+
eb := rlp.NewEncoderBuffer(w)
117+
n.encode(eb)
118+
return eb.Flush()
126119
}
127120

128121
// rawShortNode represents only the useful data content of a short node, with the
@@ -164,11 +157,7 @@ func (n *cachedNode) rlp() []byte {
164157
if node, ok := n.node.(rawNode); ok {
165158
return node
166159
}
167-
blob, err := rlp.EncodeToBytes(n.node)
168-
if err != nil {
169-
panic(err)
170-
}
171-
return blob
160+
return nodeToBytes(n.node)
172161
}
173162

174163
// obj returns the decoded and expanded trie node, either directly from the cache,

trie/hasher.go

+29-27
Original file line numberDiff line numberDiff line change
@@ -24,31 +24,22 @@ import (
2424
"golang.org/x/crypto/sha3"
2525
)
2626

27-
type sliceBuffer []byte
28-
29-
func (b *sliceBuffer) Write(data []byte) (n int, err error) {
30-
*b = append(*b, data...)
31-
return len(data), nil
32-
}
33-
34-
func (b *sliceBuffer) Reset() {
35-
*b = (*b)[:0]
36-
}
37-
3827
// hasher is a type used for the trie Hash operation. A hasher has some
3928
// internal preallocated temp space
4029
type hasher struct {
4130
sha crypto.KeccakState
42-
tmp sliceBuffer
31+
tmp []byte
32+
encbuf rlp.EncoderBuffer
4333
parallel bool // Whether to use paralallel threads when hashing
4434
}
4535

4636
// hasherPool holds pureHashers
4737
var hasherPool = sync.Pool{
4838
New: func() interface{} {
4939
return &hasher{
50-
tmp: make(sliceBuffer, 0, 550), // cap is as large as a full fullNode.
51-
sha: sha3.NewLegacyKeccak256().(crypto.KeccakState),
40+
tmp: make([]byte, 0, 550), // cap is as large as a full fullNode.
41+
sha: sha3.NewLegacyKeccak256().(crypto.KeccakState),
42+
encbuf: rlp.NewEncoderBuffer(nil),
5243
}
5344
},
5445
}
@@ -153,30 +144,41 @@ func (h *hasher) hashFullNodeChildren(n *fullNode) (collapsed *fullNode, cached
153144
// into compact form for RLP encoding.
154145
// If the rlp data is smaller than 32 bytes, `nil` is returned.
155146
func (h *hasher) shortnodeToHash(n *shortNode, force bool) node {
156-
h.tmp.Reset()
157-
if err := rlp.Encode(&h.tmp, n); err != nil {
158-
panic("encode error: " + err.Error())
159-
}
147+
n.encode(h.encbuf)
148+
enc := h.encodedBytes()
160149

161-
if len(h.tmp) < 32 && !force {
150+
if len(enc) < 32 && !force {
162151
return n // Nodes smaller than 32 bytes are stored inside their parent
163152
}
164-
return h.hashData(h.tmp)
153+
return h.hashData(enc)
165154
}
166155

167156
// shortnodeToHash is used to creates a hashNode from a set of hashNodes, (which
168157
// may contain nil values)
169158
func (h *hasher) fullnodeToHash(n *fullNode, force bool) node {
170-
h.tmp.Reset()
171-
// Generate the RLP encoding of the node
172-
if err := n.EncodeRLP(&h.tmp); err != nil {
173-
panic("encode error: " + err.Error())
174-
}
159+
n.encode(h.encbuf)
160+
enc := h.encodedBytes()
175161

176-
if len(h.tmp) < 32 && !force {
162+
if len(enc) < 32 && !force {
177163
return n // Nodes smaller than 32 bytes are stored inside their parent
178164
}
179-
return h.hashData(h.tmp)
165+
return h.hashData(enc)
166+
}
167+
168+
// encodedBytes returns the result of the last encoding operation on h.encbuf.
169+
// This also resets the encoder buffer.
170+
//
171+
// All node encoding must be done like this:
172+
//
173+
// node.encode(h.encbuf)
174+
// enc := h.encodedBytes()
175+
//
176+
// This convention exists because node.encode can only be inlined/escape-analyzed when
177+
// called on a concrete receiver type.
178+
func (h *hasher) encodedBytes() []byte {
179+
h.tmp = h.encbuf.AppendToBytes(h.tmp[:0])
180+
h.encbuf.Reset(nil)
181+
return h.tmp
180182
}
181183

182184
// hashData hashes the provided data

trie/iterator.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323

2424
"github.com/ethereum/go-ethereum/common"
2525
"github.com/ethereum/go-ethereum/ethdb"
26-
"github.com/ethereum/go-ethereum/rlp"
2726
)
2827

2928
// Iterator is a key-value trie iterator that traverses a Trie.
@@ -214,8 +213,7 @@ func (it *nodeIterator) LeafProof() [][]byte {
214213
// Gather nodes that end up as hash nodes (or the root)
215214
node, hashed := hasher.proofHash(item.node)
216215
if _, ok := hashed.(hashNode); ok || i == 0 {
217-
enc, _ := rlp.EncodeToBytes(node)
218-
proofs = append(proofs, enc)
216+
proofs = append(proofs, nodeToBytes(node))
219217
}
220218
}
221219
return proofs

trie/node.go

+5-11
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ import (
2828
var indices = []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "[17]"}
2929

3030
type node interface {
31-
fstring(string) string
3231
cache() (hashNode, bool)
32+
encode(w rlp.EncoderBuffer)
33+
fstring(string) string
3334
}
3435

3536
type (
@@ -52,16 +53,9 @@ var nilValueNode = valueNode(nil)
5253

5354
// EncodeRLP encodes a full node into the consensus RLP format.
5455
func (n *fullNode) EncodeRLP(w io.Writer) error {
55-
var nodes [17]node
56-
57-
for i, child := range &n.Children {
58-
if child != nil {
59-
nodes[i] = child
60-
} else {
61-
nodes[i] = nilValueNode
62-
}
63-
}
64-
return rlp.Encode(w, nodes)
56+
eb := rlp.NewEncoderBuffer(w)
57+
n.encode(eb)
58+
return eb.Flush()
6559
}
6660

6761
func (n *fullNode) copy() *fullNode { copy := *n; return &copy }

0 commit comments

Comments
 (0)