core/types, trie: reduce allocs in derivesha

This commit is contained in:
Martin Holst Swende 2024-11-12 10:04:35 +01:00
parent bfc3aaee4c
commit f52e3b7dff
No known key found for this signature in database
GPG Key ID: 683B438C05A5DDF0
5 changed files with 59 additions and 16 deletions

View File

@ -81,6 +81,9 @@ func prefixedRlpHash(prefix byte, x interface{}) (h common.Hash) {
type TrieHasher interface { type TrieHasher interface {
Reset() Reset()
Update([]byte, []byte) error Update([]byte, []byte) error
// UpdateSafe is identical to Update, except that this method will copy the
// value slice. The caller is free to modify the value bytes after this method returns.
UpdateSafe([]byte, []byte) error
Hash() common.Hash Hash() common.Hash
} }
@ -95,10 +98,7 @@ type DerivableList interface {
func encodeForDerive(list DerivableList, i int, buf *bytes.Buffer) []byte { func encodeForDerive(list DerivableList, i int, buf *bytes.Buffer) []byte {
buf.Reset() buf.Reset()
list.EncodeIndex(i, buf) list.EncodeIndex(i, buf)
// It's really unfortunate that we need to perform this copy. return buf.Bytes()
// StackTrie holds onto the values until Hash is called, so the values
// written to it must not alias.
return common.CopyBytes(buf.Bytes())
} }
// DeriveSha creates the tree hashes of transactions, receipts, and withdrawals in a block header. // DeriveSha creates the tree hashes of transactions, receipts, and withdrawals in a block header.
@ -118,17 +118,17 @@ func DeriveSha(list DerivableList, hasher TrieHasher) common.Hash {
for i := 1; i < list.Len() && i <= 0x7f; i++ { for i := 1; i < list.Len() && i <= 0x7f; i++ {
indexBuf = rlp.AppendUint64(indexBuf[:0], uint64(i)) indexBuf = rlp.AppendUint64(indexBuf[:0], uint64(i))
value := encodeForDerive(list, i, valueBuf) value := encodeForDerive(list, i, valueBuf)
hasher.Update(indexBuf, value) hasher.UpdateSafe(indexBuf, value)
} }
if list.Len() > 0 { if list.Len() > 0 {
indexBuf = rlp.AppendUint64(indexBuf[:0], 0) indexBuf = rlp.AppendUint64(indexBuf[:0], 0)
value := encodeForDerive(list, 0, valueBuf) value := encodeForDerive(list, 0, valueBuf)
hasher.Update(indexBuf, value) hasher.UpdateSafe(indexBuf, value)
} }
for i := 0x80; i < list.Len(); i++ { for i := 0x80; i < list.Len(); i++ {
indexBuf = rlp.AppendUint64(indexBuf[:0], uint64(i)) indexBuf = rlp.AppendUint64(indexBuf[:0], uint64(i))
value := encodeForDerive(list, i, valueBuf) value := encodeForDerive(list, i, valueBuf)
hasher.Update(indexBuf, value) hasher.UpdateSafe(indexBuf, value)
} }
return hasher.Hash() return hasher.Hash()
} }

View File

@ -81,13 +81,16 @@ func BenchmarkDeriveSha200(b *testing.B) {
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
var exp common.Hash want := types.DeriveSha(txs, trie.NewEmpty(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)))
var got common.Hash var have common.Hash
b.Run("std_trie", func(b *testing.B) { b.Run("std_trie", func(b *testing.B) {
b.ResetTimer() b.ResetTimer()
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
exp = types.DeriveSha(txs, trie.NewEmpty(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil))) have = types.DeriveSha(txs, trie.NewEmpty(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)))
}
if have != want {
b.Errorf("have %x want %x", have, want)
} }
}) })
@ -95,12 +98,12 @@ func BenchmarkDeriveSha200(b *testing.B) {
b.ResetTimer() b.ResetTimer()
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
got = types.DeriveSha(txs, trie.NewStackTrie(nil)) have = types.DeriveSha(txs, trie.NewStackTrie(nil))
}
if have != want {
b.Errorf("have %x want %x", have, want)
} }
}) })
if got != exp {
b.Errorf("got %x exp %x", got, exp)
}
} }
func TestFuzzDeriveSha(t *testing.T) { func TestFuzzDeriveSha(t *testing.T) {
@ -226,6 +229,12 @@ func (d *hashToHumanReadable) Update(i []byte, i2 []byte) error {
return nil return nil
} }
// UpdateSafe is identical to Update, except that this method will copy the
// value slice. The caller is free to modify the value bytes after this method returns.
func (d *hashToHumanReadable) UpdateSafe(key, value []byte) error {
return d.Update(key, common.CopyBytes(value))
}
func (d *hashToHumanReadable) Hash() common.Hash { func (d *hashToHumanReadable) Hash() common.Hash {
return common.Hash{} return common.Hash{}
} }

View File

@ -53,6 +53,12 @@ func (h *testHasher) Update(key, val []byte) error {
return nil return nil
} }
// UpdateSafe is identical to Update, except that this method will copy the
// value slice. The caller is free to modify the value bytes after this method returns.
func (h *testHasher) UpdateSafe(key, value []byte) error {
return h.Update(key, common.CopyBytes(value))
}
// Hash returns the hash value. // Hash returns the hash value.
func (h *testHasher) Hash() common.Hash { func (h *testHasher) Hash() common.Hash {
return common.BytesToHash(h.hasher.Sum(nil)) return common.BytesToHash(h.hasher.Sum(nil))

View File

@ -50,6 +50,7 @@ type StackTrie struct {
onTrieNode OnTrieNode onTrieNode OnTrieNode
kBuf []byte // buf space used for hex-key during insertions kBuf []byte // buf space used for hex-key during insertions
pBuf []byte // buf space used for path during insertions pBuf []byte // buf space used for path during insertions
vPool *bytesPool
} }
// NewStackTrie allocates and initializes an empty trie. The committed nodes // NewStackTrie allocates and initializes an empty trie. The committed nodes
@ -61,10 +62,20 @@ func NewStackTrie(onTrieNode OnTrieNode) *StackTrie {
onTrieNode: onTrieNode, onTrieNode: onTrieNode,
kBuf: make([]byte, 0, 64), kBuf: make([]byte, 0, 64),
pBuf: make([]byte, 0, 32), pBuf: make([]byte, 0, 32),
vPool: newBytesPool(300, 20),
} }
} }
// UpdateSafe is identical to Update, except that this method will copy the
// value slice. The caller is free to modify the value bytes after this method returns.
func (t *StackTrie) UpdateSafe(key, value []byte) error {
// The stacktrie always copies the value (is already safe).
return t.Update(key, value)
}
// Update inserts a (key, value) pair into the stack trie. // Update inserts a (key, value) pair into the stack trie.
// The value is copied, and the caller is free to modify the value after this
// method returns.
func (t *StackTrie) Update(key, value []byte) error { func (t *StackTrie) Update(key, value []byte) error {
if len(value) == 0 { if len(value) == 0 {
return errors.New("trying to insert empty (deletion)") return errors.New("trying to insert empty (deletion)")
@ -87,7 +98,14 @@ func (t *StackTrie) Update(key, value []byte) error {
} else { } else {
t.last = append(t.last[:0], k...) // reuse key slice t.last = append(t.last[:0], k...) // reuse key slice
} }
t.insert(t.root, k, value, t.pBuf[:0]) vBuf := t.vPool.Get()
if cap(vBuf) < len(value) {
vBuf = common.CopyBytes(value)
} else {
vBuf = vBuf[:len(value)]
copy(vBuf, value)
}
t.insert(t.root, k, vBuf, t.pBuf[:0])
return nil return nil
} }
@ -392,7 +410,11 @@ func (t *StackTrie) hash(st *stNode, path []byte) {
st.typ = hashedNode st.typ = hashedNode
st.key = st.key[:0] st.key = st.key[:0]
st.val = nil // Release reference to potentially externally held slice. // Release reference to (potentially externally held) value-slice.
if cap(st.val) > 0 && t.vPool != nil {
t.vPool.Put(st.val)
}
st.val = nil
// Skip committing the non-root node if the size is smaller than 32 bytes // Skip committing the non-root node if the size is smaller than 32 bytes
// as tiny nodes are always embedded in their parent except root node. // as tiny nodes are always embedded in their parent except root node.

View File

@ -308,6 +308,12 @@ func (t *Trie) Update(key, value []byte) error {
return t.update(key, value) return t.update(key, value)
} }
// UpdateSafe is identical to Update, except that this method will copy the
// value slice. The caller is free to modify the value bytes after this method returns.
func (t *Trie) UpdateSafe(key, value []byte) error {
return t.Update(key, common.CopyBytes(value))
}
func (t *Trie) update(key, value []byte) error { func (t *Trie) update(key, value []byte) error {
t.unhashed++ t.unhashed++
t.uncommitted++ t.uncommitted++