Skip to content

Commit 0576671

Browse files
holimanrjl493456442fjl
authored
core/types, trie: reduce allocations in derivesha (#30747)
Alternative to #30746, potential follow-up to #30743 . This PR makes the stacktrie always copy incoming value buffers, and reuse them internally. Improvement in #30743: ``` goos: linux goarch: amd64 pkg: github.com/ethereum/go-ethereum/core/types cpu: 12th Gen Intel(R) Core(TM) i7-1270P │ derivesha.1 │ derivesha.2 │ │ sec/op │ sec/op vs base │ DeriveSha200/stack_trie-8 477.8µ ± 2% 430.0µ ± 12% -10.00% (p=0.000 n=10) │ derivesha.1 │ derivesha.2 │ │ B/op │ B/op vs base │ DeriveSha200/stack_trie-8 45.17Ki ± 0% 25.65Ki ± 0% -43.21% (p=0.000 n=10) │ derivesha.1 │ derivesha.2 │ │ allocs/op │ allocs/op vs base │ DeriveSha200/stack_trie-8 1259.0 ± 0% 232.0 ± 0% -81.57% (p=0.000 n=10) ``` This PR further enhances that: ``` goos: linux goarch: amd64 pkg: github.com/ethereum/go-ethereum/core/types cpu: 12th Gen Intel(R) Core(TM) i7-1270P │ derivesha.2 │ derivesha.3 │ │ sec/op │ sec/op vs base │ DeriveSha200/stack_trie-8 430.0µ ± 12% 423.6µ ± 13% ~ (p=0.739 n=10) │ derivesha.2 │ derivesha.3 │ │ B/op │ B/op vs base │ DeriveSha200/stack_trie-8 25.654Ki ± 0% 4.960Ki ± 0% -80.67% (p=0.000 n=10) │ derivesha.2 │ derivesha.3 │ │ allocs/op │ allocs/op vs base │ DeriveSha200/stack_trie-8 232.00 ± 0% 37.00 ± 0% -84.05% (p=0.000 n=10) ``` So the total derivesha-improvement over *both PRS* is: ``` goos: linux goarch: amd64 pkg: github.com/ethereum/go-ethereum/core/types cpu: 12th Gen Intel(R) Core(TM) i7-1270P │ derivesha.1 │ derivesha.3 │ │ sec/op │ sec/op vs base │ DeriveSha200/stack_trie-8 477.8µ ± 2% 423.6µ ± 13% -11.33% (p=0.015 n=10) │ derivesha.1 │ derivesha.3 │ │ B/op │ B/op vs base │ DeriveSha200/stack_trie-8 45.171Ki ± 0% 4.960Ki ± 0% -89.02% (p=0.000 n=10) │ derivesha.1 │ derivesha.3 │ │ allocs/op │ allocs/op vs base │ DeriveSha200/stack_trie-8 1259.00 ± 0% 37.00 ± 0% -97.06% (p=0.000 n=10) ``` Since this PR always copies the incoming value, it adds a little bit of a penalty on the previous insert-benchmark, which copied nothing (always passed the same empty slice as input) : ``` goos: linux goarch: amd64 pkg: github.com/ethereum/go-ethereum/trie cpu: 12th Gen Intel(R) Core(TM) i7-1270P │ stacktrie.7 │ stacktrie.10 │ │ sec/op │ sec/op vs base │ Insert100K-8 88.21m ± 34% 92.37m ± 31% ~ (p=0.280 n=10) │ stacktrie.7 │ stacktrie.10 │ │ B/op │ B/op vs base │ Insert100K-8 3.424Ki ± 3% 4.581Ki ± 3% +33.80% (p=0.000 n=10) │ stacktrie.7 │ stacktrie.10 │ │ allocs/op │ allocs/op vs base │ Insert100K-8 22.00 ± 5% 26.00 ± 4% +18.18% (p=0.000 n=10) ``` --------- Co-authored-by: Gary Rong <[email protected]> Co-authored-by: Felix Lange <[email protected]>
1 parent 1487a85 commit 0576671

File tree

8 files changed

+189
-65
lines changed

8 files changed

+189
-65
lines changed

core/types/block.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ type extblock struct {
240240
//
241241
// The receipt's bloom must already calculated for the block's bloom to be
242242
// correctly calculated.
243-
func NewBlock(header *Header, body *Body, receipts []*Receipt, hasher TrieHasher) *Block {
243+
func NewBlock(header *Header, body *Body, receipts []*Receipt, hasher ListHasher) *Block {
244244
if body == nil {
245245
body = &Body{}
246246
}

core/types/hashing.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727
"github.com/ethereum/go-ethereum/rlp"
2828
)
2929

30-
// hasherPool holds LegacyKeccak256 hashers for rlpHash.
30+
// hasherPool holds LegacyKeccak256 buffer for rlpHash.
3131
var hasherPool = sync.Pool{
3232
New: func() interface{} { return crypto.NewKeccakState() },
3333
}
@@ -75,11 +75,17 @@ func prefixedRlpHash(prefix byte, x interface{}) (h common.Hash) {
7575
return h
7676
}
7777

78-
// TrieHasher is the tool used to calculate the hash of derivable list.
79-
// This is internal, do not use.
80-
type TrieHasher interface {
78+
// ListHasher defines the interface for computing the hash of a derivable list.
79+
type ListHasher interface {
80+
// Reset clears the internal state of the hasher, preparing it for reuse.
8181
Reset()
82-
Update([]byte, []byte) error
82+
83+
// Update inserts the given key-value pair into the hasher.
84+
// The implementation must copy the provided slices, allowing the caller
85+
// to safely modify them after the call returns.
86+
Update(key []byte, value []byte) error
87+
88+
// Hash computes and returns the final hash of all inserted key-value pairs.
8389
Hash() common.Hash
8490
}
8591

@@ -91,19 +97,20 @@ type DerivableList interface {
9197
EncodeIndex(int, *bytes.Buffer)
9298
}
9399

100+
// encodeForDerive encodes the element in the list at the position i into the buffer.
94101
func encodeForDerive(list DerivableList, i int, buf *bytes.Buffer) []byte {
95102
buf.Reset()
96103
list.EncodeIndex(i, buf)
97-
// It's really unfortunate that we need to perform this copy.
98-
// StackTrie holds onto the values until Hash is called, so the values
99-
// written to it must not alias.
100-
return common.CopyBytes(buf.Bytes())
104+
return buf.Bytes()
101105
}
102106

103107
// DeriveSha creates the tree hashes of transactions, receipts, and withdrawals in a block header.
104-
func DeriveSha(list DerivableList, hasher TrieHasher) common.Hash {
108+
func DeriveSha(list DerivableList, hasher ListHasher) common.Hash {
105109
hasher.Reset()
106110

111+
// Allocate a buffer for value encoding. As the hasher is claimed that all
112+
// supplied key value pairs will be copied by hasher and safe to reuse the
113+
// encoding buffer.
107114
valueBuf := encodeBufferPool.Get().(*bytes.Buffer)
108115
defer encodeBufferPool.Put(valueBuf)
109116

core/types/hashing_test.go

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,10 @@ import (
2626

2727
"github.com/ethereum/go-ethereum/common"
2828
"github.com/ethereum/go-ethereum/common/hexutil"
29-
"github.com/ethereum/go-ethereum/core/rawdb"
3029
"github.com/ethereum/go-ethereum/core/types"
3130
"github.com/ethereum/go-ethereum/crypto"
3231
"github.com/ethereum/go-ethereum/rlp"
3332
"github.com/ethereum/go-ethereum/trie"
34-
"github.com/ethereum/go-ethereum/triedb"
3533
)
3634

3735
func TestDeriveSha(t *testing.T) {
@@ -40,7 +38,7 @@ func TestDeriveSha(t *testing.T) {
4038
t.Fatal(err)
4139
}
4240
for len(txs) < 1000 {
43-
exp := types.DeriveSha(txs, trie.NewEmpty(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)))
41+
exp := types.DeriveSha(txs, trie.NewListHasher())
4442
got := types.DeriveSha(txs, trie.NewStackTrie(nil))
4543
if !bytes.Equal(got[:], exp[:]) {
4644
t.Fatalf("%d txs: got %x exp %x", len(txs), got, exp)
@@ -76,38 +74,53 @@ func TestEIP2718DeriveSha(t *testing.T) {
7674
}
7775
}
7876

77+
// goos: darwin
78+
// goarch: arm64
79+
// pkg: github.com/ethereum/go-ethereum/core/types
80+
// cpu: Apple M1 Pro
81+
// BenchmarkDeriveSha200
82+
// BenchmarkDeriveSha200/std_trie
83+
// BenchmarkDeriveSha200/std_trie-8 6754 174074 ns/op 80054 B/op 1926 allocs/op
84+
// BenchmarkDeriveSha200/stack_trie
85+
// BenchmarkDeriveSha200/stack_trie-8 7296 162675 ns/op 745 B/op 19 allocs/op
7986
func BenchmarkDeriveSha200(b *testing.B) {
8087
txs, err := genTxs(200)
8188
if err != nil {
8289
b.Fatal(err)
8390
}
84-
var exp common.Hash
85-
var got common.Hash
91+
want := types.DeriveSha(txs, trie.NewListHasher())
92+
8693
b.Run("std_trie", func(b *testing.B) {
8794
b.ReportAllocs()
95+
var have common.Hash
8896
for b.Loop() {
89-
exp = types.DeriveSha(txs, trie.NewEmpty(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)))
97+
have = types.DeriveSha(txs, trie.NewListHasher())
98+
}
99+
if have != want {
100+
b.Errorf("have %x want %x", have, want)
90101
}
91102
})
92103

104+
st := trie.NewStackTrie(nil)
93105
b.Run("stack_trie", func(b *testing.B) {
94-
b.ResetTimer()
95106
b.ReportAllocs()
107+
var have common.Hash
96108
for b.Loop() {
97-
got = types.DeriveSha(txs, trie.NewStackTrie(nil))
109+
st.Reset()
110+
have = types.DeriveSha(txs, st)
111+
}
112+
if have != want {
113+
b.Errorf("have %x want %x", have, want)
98114
}
99115
})
100-
if got != exp {
101-
b.Errorf("got %x exp %x", got, exp)
102-
}
103116
}
104117

105118
func TestFuzzDeriveSha(t *testing.T) {
106119
// increase this for longer runs -- it's set to quite low for travis
107120
rndSeed := mrand.Int()
108121
for i := 0; i < 10; i++ {
109122
seed := rndSeed + i
110-
exp := types.DeriveSha(newDummy(i), trie.NewEmpty(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)))
123+
exp := types.DeriveSha(newDummy(i), trie.NewListHasher())
111124
got := types.DeriveSha(newDummy(i), trie.NewStackTrie(nil))
112125
if !bytes.Equal(got[:], exp[:]) {
113126
printList(t, newDummy(seed))
@@ -135,7 +148,7 @@ func TestDerivableList(t *testing.T) {
135148
},
136149
}
137150
for i, tc := range tcs[1:] {
138-
exp := types.DeriveSha(flatList(tc), trie.NewEmpty(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)))
151+
exp := types.DeriveSha(flatList(tc), trie.NewListHasher())
139152
got := types.DeriveSha(flatList(tc), trie.NewStackTrie(nil))
140153
if !bytes.Equal(got[:], exp[:]) {
141154
t.Fatalf("case %d: got %x exp %x", i, got, exp)

internal/blocktest/test_hash.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
package blocktest
2424

2525
import (
26+
"bytes"
2627
"hash"
2728

2829
"github.com/ethereum/go-ethereum/common"
@@ -48,8 +49,8 @@ func (h *testHasher) Reset() {
4849

4950
// Update updates the hash state with the given key and value.
5051
func (h *testHasher) Update(key, val []byte) error {
51-
h.hasher.Write(key)
52-
h.hasher.Write(val)
52+
h.hasher.Write(bytes.Clone(key))
53+
h.hasher.Write(bytes.Clone(val))
5354
return nil
5455
}
5556

trie/bytepool.go

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ func newBytesPool(sliceCap, nitems int) *bytesPool {
3232
}
3333
}
3434

35-
// Get returns a slice. Safe for concurrent use.
36-
func (bp *bytesPool) Get() []byte {
35+
// get returns a slice. Safe for concurrent use.
36+
func (bp *bytesPool) get() []byte {
3737
select {
3838
case b := <-bp.c:
3939
return b
@@ -42,18 +42,18 @@ func (bp *bytesPool) Get() []byte {
4242
}
4343
}
4444

45-
// GetWithSize returns a slice with specified byte slice size.
46-
func (bp *bytesPool) GetWithSize(s int) []byte {
47-
b := bp.Get()
45+
// getWithSize returns a slice with specified byte slice size.
46+
func (bp *bytesPool) getWithSize(s int) []byte {
47+
b := bp.get()
4848
if cap(b) < s {
4949
return make([]byte, s)
5050
}
5151
return b[:s]
5252
}
5353

54-
// Put returns a slice to the pool. Safe for concurrent use. This method
54+
// put returns a slice to the pool. Safe for concurrent use. This method
5555
// will ignore slices that are too small or too large (>3x the cap)
56-
func (bp *bytesPool) Put(b []byte) {
56+
func (bp *bytesPool) put(b []byte) {
5757
if c := cap(b); c < bp.w || c > 3*bp.w {
5858
return
5959
}
@@ -62,3 +62,40 @@ func (bp *bytesPool) Put(b []byte) {
6262
default:
6363
}
6464
}
65+
66+
// unsafeBytesPool is a pool for byte slices. It is not safe for concurrent use.
67+
type unsafeBytesPool struct {
68+
items [][]byte
69+
w int
70+
}
71+
72+
// newUnsafeBytesPool creates a new unsafeBytesPool. The sliceCap sets the
73+
// capacity of newly allocated slices, and the nitems determines how many
74+
// items the pool will hold, at maximum.
75+
func newUnsafeBytesPool(sliceCap, nitems int) *unsafeBytesPool {
76+
return &unsafeBytesPool{
77+
items: make([][]byte, 0, nitems),
78+
w: sliceCap,
79+
}
80+
}
81+
82+
// Get returns a slice with pre-allocated space.
83+
func (bp *unsafeBytesPool) get() []byte {
84+
if len(bp.items) > 0 {
85+
last := bp.items[len(bp.items)-1]
86+
bp.items = bp.items[:len(bp.items)-1]
87+
return last
88+
}
89+
return make([]byte, 0, bp.w)
90+
}
91+
92+
// put returns a slice to the pool. This method will ignore slices that are
93+
// too small or too large (>3x the cap)
94+
func (bp *unsafeBytesPool) put(b []byte) {
95+
if c := cap(b); c < bp.w || c > 3*bp.w {
96+
return
97+
}
98+
if len(bp.items) < cap(bp.items) {
99+
bp.items = append(bp.items, b)
100+
}
101+
}

trie/list_hasher.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2025 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package trie
18+
19+
import (
20+
"bytes"
21+
22+
"github.com/ethereum/go-ethereum/common"
23+
)
24+
25+
// ListHasher is a wrapper of the Merkle-Patricia-Trie, which implements
26+
// types.ListHasher. Compared to a Trie instance, the Update method of this
27+
// type always deep-copies its input slices.
28+
//
29+
// This implementation is very inefficient in terms of memory allocation,
30+
// compared with StackTrie. It exists only for correctness comparison purposes.
31+
type ListHasher struct {
32+
tr *Trie
33+
}
34+
35+
// NewListHasher initializes the list hasher.
36+
func NewListHasher() *ListHasher {
37+
return &ListHasher{
38+
tr: NewEmpty(nil),
39+
}
40+
}
41+
42+
// Reset clears the internal state prepares the ListHasher for reuse.
43+
func (h *ListHasher) Reset() {
44+
h.tr.reset()
45+
}
46+
47+
// Update inserts a key-value pair into the trie.
48+
func (h *ListHasher) Update(key []byte, value []byte) error {
49+
key, value = bytes.Clone(key), bytes.Clone(value)
50+
return h.tr.Update(key, value)
51+
}
52+
53+
// Hash computes the root hash of all inserted key-value pairs.
54+
func (h *ListHasher) Hash() common.Hash {
55+
return h.tr.Hash()
56+
}

0 commit comments

Comments
 (0)