trie: parallize committer

This commit is contained in:
stevemilk 2024-09-18 23:47:12 +08:00
parent 0dd7e82c0a
commit 7c25f135ef
6 changed files with 130 additions and 47 deletions

View file

@ -945,7 +945,7 @@ func (s *StateDB) fastDeleteStorage(snaps *snapshot.Tree, addrHash common.Hash,
slots = make(map[common.Hash][]byte) slots = make(map[common.Hash][]byte)
) )
stack := trie.NewStackTrie(func(path []byte, hash common.Hash, blob []byte) { stack := trie.NewStackTrie(func(path []byte, hash common.Hash, blob []byte) {
nodes.AddNode(path, trienode.NewDeleted()) nodes.AddNode(string(path), trienode.NewDeleted())
}) })
for iter.Next() { for iter.Next() {
slot := common.CopyBytes(iter.Slot()) slot := common.CopyBytes(iter.Slot())
@ -991,7 +991,7 @@ func (s *StateDB) slowDeleteStorage(addr common.Address, addrHash common.Hash, r
if it.Hash() == (common.Hash{}) { if it.Hash() == (common.Hash{}) {
continue continue
} }
nodes.AddNode(it.Path(), trienode.NewDeleted()) nodes.AddNode(string(it.Path()), trienode.NewDeleted())
} }
if err := it.Error(); err != nil { if err := it.Error(); err != nil {
return nil, nil, err return nil, nil, err

View file

@ -18,6 +18,8 @@ package trie
import ( import (
"fmt" "fmt"
"runtime"
"sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/trienode"
@ -30,28 +32,44 @@ type committer struct {
nodes *trienode.NodeSet nodes *trienode.NodeSet
tracer *tracer tracer *tracer
collectLeaf bool collectLeaf bool
parallel bool
} }
// newCommitter creates a new committer or picks one from the pool. // newCommitter creates a new committer or picks one from the pool.
func newCommitter(nodeset *trienode.NodeSet, tracer *tracer, collectLeaf bool) *committer { func newCommitter(nodes *trienode.NodeSet, tracer *tracer, collectLeaf bool, parallel bool) *committer {
return &committer{ return &committer{
nodes: nodeset, nodes: nodes,
tracer: tracer, tracer: tracer,
collectLeaf: collectLeaf, collectLeaf: collectLeaf,
parallel: parallel,
} }
} }
type wrapNode struct {
node *trienode.Node
path string
leafHash common.Hash // optional, the parent hash of the relative leaf
leafBlob []byte // optional, the blob of the relative leaf
}
// Commit collapses a node down into a hash node. // Commit collapses a node down into a hash node.
func (c *committer) Commit(n node) hashNode { func (c *committer) Commit(n node) hashNode {
return c.commit(nil, n).(hashNode) hn, wnodes := c.commit(nil, n, true)
for _, wn := range wnodes {
c.nodes.AddNode(wn.path, wn.node)
if wn.leafHash != (common.Hash{}) {
c.nodes.AddLeaf(wn.leafHash, wn.leafBlob)
}
}
return hn.(hashNode)
} }
// commit collapses a node down into a hash node and returns it. // commit collapses a node down into a hash node and returns it.
func (c *committer) commit(path []byte, n node) node { func (c *committer) commit(path []byte, n node, topmost bool) (node, []*wrapNode) {
// if this path is clean, use available cached data // if this path is clean, use available cached data
hash, dirty := n.cache() hash, dirty := n.cache()
if hash != nil && !dirty { if hash != nil && !dirty {
return hash return hash, nil
} }
// Commit children, then parent, and remove the dirty flag. // Commit children, then parent, and remove the dirty flag.
switch cn := n.(type) { switch cn := n.(type) {
@ -61,38 +79,72 @@ func (c *committer) commit(path []byte, n node) node {
// If the child is fullNode, recursively commit, // If the child is fullNode, recursively commit,
// otherwise it can only be hashNode or valueNode. // otherwise it can only be hashNode or valueNode.
var nodes []*wrapNode
if _, ok := cn.Val.(*fullNode); ok { if _, ok := cn.Val.(*fullNode); ok {
collapsed.Val = c.commit(append(path, cn.Key...), cn.Val) collapsed.Val, nodes = c.commit(append(path, cn.Key...), cn.Val, false)
} }
// The key needs to be copied, since we're adding it to the // The key needs to be copied, since we're adding it to the
// modified nodeset. // modified nodeset.
collapsed.Key = hexToCompact(cn.Key) collapsed.Key = hexToCompact(cn.Key)
hashedNode := c.store(path, collapsed) hashedNode, wNode := c.store(path, collapsed)
if hn, ok := hashedNode.(hashNode); ok { if wNode != nil {
return hn nodes = append(nodes, wNode)
} }
return collapsed if hn, ok := hashedNode.(hashNode); ok {
return hn, nodes
}
return collapsed, nodes
case *fullNode: case *fullNode:
hashedKids := c.commitChildren(path, cn) hashedKids, nodes := c.commitChildren(path, cn, topmost && c.parallel)
collapsed := cn.copy() collapsed := cn.copy()
collapsed.Children = hashedKids collapsed.Children = hashedKids
hashedNode := c.store(path, collapsed) hashedNode, wNode := c.store(path, collapsed)
if hn, ok := hashedNode.(hashNode); ok { if wNode != nil {
return hn nodes = append(nodes, wNode)
} }
return collapsed if hn, ok := hashedNode.(hashNode); ok {
return hn, nodes
}
return collapsed, nodes
case hashNode: case hashNode:
return cn return cn, nil
default: default:
// nil, valuenode shouldn't be committed // nil, valuenode shouldn't be committed
panic(fmt.Sprintf("%T: invalid node: %v", n, n)) panic(fmt.Sprintf("%T: invalid node: %v", n, n))
} }
} }
type task struct {
node node
index int
path []byte
}
// commitChildren commits the children of the given fullnode // commitChildren commits the children of the given fullnode
func (c *committer) commitChildren(path []byte, n *fullNode) [17]node { func (c *committer) commitChildren(path []byte, n *fullNode, parallel bool) ([17]node, []*wrapNode) {
var children [17]node var (
wg sync.WaitGroup
children [17]node
results [16][]*wrapNode
tasks = make(chan task)
)
if parallel {
worker := func() {
defer wg.Done()
for t := range tasks {
children[t.index], results[t.index] = c.commit(t.path, t.node, false)
}
}
threads := runtime.NumCPU()
if threads > 16 {
threads = 16
}
for i := 0; i < threads; i++ {
wg.Add(1)
go worker()
}
}
for i := 0; i < 16; i++ { for i := 0; i < 16; i++ {
child := n.Children[i] child := n.Children[i]
if child == nil { if child == nil {
@ -108,18 +160,36 @@ func (c *committer) commitChildren(path []byte, n *fullNode) [17]node {
// Commit the child recursively and store the "hashed" value. // Commit the child recursively and store the "hashed" value.
// Note the returned node can be some embedded nodes, so it's // Note the returned node can be some embedded nodes, so it's
// possible the type is not hashNode. // possible the type is not hashNode.
children[i] = c.commit(append(path, byte(i)), child) if !parallel {
children[i], results[i] = c.commit(append(path, byte(i)), child, false)
} else {
tasks <- task{
index: i,
node: child,
path: append(path, byte(i)),
}
}
}
if parallel {
close(tasks)
wg.Wait()
} }
// For the 17th child, it's possible the type is valuenode. // For the 17th child, it's possible the type is valuenode.
if n.Children[16] != nil { if n.Children[16] != nil {
children[16] = n.Children[16] children[16] = n.Children[16]
} }
return children var wnodes []*wrapNode
for i := 0; i < 16; i++ {
if results[i] != nil {
wnodes = append(wnodes, results[i]...)
}
}
return children, wnodes
} }
// store hashes the node n and adds it to the modified nodeset. If leaf collection // store hashes the node n and adds it to the modified nodeset. If leaf collection
// is enabled, leaf nodes will be tracked in the modified nodeset as well. // is enabled, leaf nodes will be tracked in the modified nodeset as well.
func (c *committer) store(path []byte, n node) node { func (c *committer) store(path []byte, n node) (node, *wrapNode) {
// Larger nodes are replaced by their hash and stored in the database. // Larger nodes are replaced by their hash and stored in the database.
var hash, _ = n.cache() var hash, _ = n.cache()
@ -133,25 +203,33 @@ func (c *committer) store(path []byte, n node) node {
// deleted only if the node was existent in database before. // deleted only if the node was existent in database before.
_, ok := c.tracer.accessList[string(path)] _, ok := c.tracer.accessList[string(path)]
if ok { if ok {
c.nodes.AddNode(path, trienode.NewDeleted()) return n, &wrapNode{
path: string(path),
node: trienode.NewDeleted(),
} }
return n
} }
// Collect the dirty node to nodeset for return. return n, nil
}
nhash := common.BytesToHash(hash) nhash := common.BytesToHash(hash)
c.nodes.AddNode(path, trienode.New(nhash, nodeToBytes(n))) wNode := &wrapNode{
path: string(path),
node: trienode.New(nhash, nodeToBytes(n)),
}
// Collect the corresponding leaf node if it's required. We don't check // Collect the corresponding leaf node if it's required. We don't check
// full node since it's impossible to store value in fullNode. The key // full node since it's impossible to store value in fullNode. The key
// length of leaves should be exactly same. // length of leaves should be exactly same..
if c.collectLeaf { if c.collectLeaf {
if sn, ok := n.(*shortNode); ok { if sn, ok := n.(*shortNode); ok {
if val, ok := sn.Val.(valueNode); ok { if val, ok := sn.Val.(valueNode); ok {
c.nodes.AddLeaf(nhash, val) c.nodes.AddLeaf(nhash, val)
wNode.leafHash = nhash
wNode.leafBlob = val
} }
} }
} }
return hash
return hash, wNode
} }
// ForGatherChildren decodes the provided node and traverses the children inside. // ForGatherChildren decodes the provided node and traverses the children inside.

View file

@ -44,16 +44,17 @@ type Trie struct {
// trie is not usable(latest states is invisible). // trie is not usable(latest states is invisible).
committed bool committed bool
// Keep track of the number leaves which have been inserted since the last
// hashing operation. This number will not directly map to the number of
// actually unhashed nodes.
unhashed int
// reader is the handler trie can retrieve nodes from. // reader is the handler trie can retrieve nodes from.
reader *trieReader reader *trieReader
// tracer is the tool to track the trie changes. // tracer is the tool to track the trie changes.
tracer *tracer tracer *tracer
// The number of trie mutations that have been performed
mutate int
// The number of mutations that have been hashed
hashed int
} }
// newFlag returns the cache flag value for a newly created node. // newFlag returns the cache flag value for a newly created node.
@ -67,9 +68,10 @@ func (t *Trie) Copy() *Trie {
root: t.root, root: t.root,
owner: t.owner, owner: t.owner,
committed: t.committed, committed: t.committed,
unhashed: t.unhashed,
reader: t.reader, reader: t.reader,
tracer: t.tracer.copy(), tracer: t.tracer.copy(),
mutate: t.mutate,
hashed: t.hashed,
} }
} }
@ -304,11 +306,12 @@ func (t *Trie) Update(key, value []byte) error {
if t.committed { if t.committed {
return ErrCommitted return ErrCommitted
} }
t.mutate++
return t.update(key, value) return t.update(key, value)
} }
func (t *Trie) update(key, value []byte) error { func (t *Trie) update(key, value []byte) error {
t.unhashed++ t.mutate++
k := keybytesToHex(key) k := keybytesToHex(key)
if len(value) != 0 { if len(value) != 0 {
_, n, err := t.insert(t.root, nil, k, valueNode(value)) _, n, err := t.insert(t.root, nil, k, valueNode(value))
@ -422,7 +425,7 @@ func (t *Trie) Delete(key []byte) error {
if t.committed { if t.committed {
return ErrCommitted return ErrCommitted
} }
t.unhashed++ t.mutate++
k := keybytesToHex(key) k := keybytesToHex(key)
_, n, err := t.delete(t.root, nil, k) _, n, err := t.delete(t.root, nil, k)
if err != nil { if err != nil {
@ -622,7 +625,7 @@ func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet) {
} }
nodes := trienode.NewNodeSet(t.owner) nodes := trienode.NewNodeSet(t.owner)
for _, path := range paths { for _, path := range paths {
nodes.AddNode([]byte(path), trienode.NewDeleted()) nodes.AddNode(path, trienode.NewDeleted())
} }
return types.EmptyRootHash, nodes // case (b) return types.EmptyRootHash, nodes // case (b)
} }
@ -640,9 +643,10 @@ func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet) {
} }
nodes := trienode.NewNodeSet(t.owner) nodes := trienode.NewNodeSet(t.owner)
for _, path := range t.tracer.deletedNodes() { for _, path := range t.tracer.deletedNodes() {
nodes.AddNode([]byte(path), trienode.NewDeleted()) nodes.AddNode(path, trienode.NewDeleted())
} }
t.root = newCommitter(nodes, t.tracer, collectLeaf).Commit(t.root) t.root = newCommitter(nodes, t.tracer, collectLeaf, t.mutate > 100).Commit(t.root)
t.mutate = 0
return rootHash, nodes return rootHash, nodes
} }
@ -652,10 +656,10 @@ func (t *Trie) hashRoot() (node, node) {
return hashNode(types.EmptyRootHash.Bytes()), nil return hashNode(types.EmptyRootHash.Bytes()), nil
} }
// If the number of changes is below 100, we let one thread handle it // If the number of changes is below 100, we let one thread handle it
h := newHasher(t.unhashed >= 100) h := newHasher(t.mutate-t.hashed >= 100)
defer func() { defer func() {
returnHasherToPool(h) returnHasherToPool(h)
t.unhashed = 0 t.hashed = t.mutate
}() }()
hashed, cached := h.hash(t.root, true) hashed, cached := h.hash(t.root, true)
return hashed, cached return hashed, cached
@ -677,7 +681,8 @@ func (t *Trie) Witness() map[string]struct{} {
func (t *Trie) Reset() { func (t *Trie) Reset() {
t.root = nil t.root = nil
t.owner = common.Hash{} t.owner = common.Hash{}
t.unhashed = 0
t.tracer.reset() t.tracer.reset()
t.committed = false t.committed = false
t.hashed = 0
t.mutate = 0
} }

View file

@ -90,13 +90,13 @@ func (set *NodeSet) ForEachWithOrder(callback func(path string, n *Node)) {
} }
// AddNode adds the provided node into set. // AddNode adds the provided node into set.
func (set *NodeSet) AddNode(path []byte, n *Node) { func (set *NodeSet) AddNode(path string, n *Node) {
if n.IsDeleted() { if n.IsDeleted() {
set.deletes += 1 set.deletes += 1
} else { } else {
set.updates += 1 set.updates += 1
} }
set.Nodes[string(path)] = n set.Nodes[path] = n
} }
// Merge adds a set of nodes into the set. // Merge adds a set of nodes into the set.

View file

@ -42,7 +42,7 @@ func benchmarkMerge(b *testing.B, count int) {
blob := make([]byte, 32) blob := make([]byte, 32)
rand.Read(blob) rand.Read(blob)
hash := crypto.Keccak256Hash(blob) hash := crypto.Keccak256Hash(blob)
s.AddNode(path, New(hash, blob)) s.AddNode(string(path), New(hash, blob))
} }
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
// Random path of 4 nibbles // Random path of 4 nibbles

View file

@ -258,7 +258,7 @@ func (t *VerkleTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) {
nodeset := trienode.NewNodeSet(common.Hash{}) nodeset := trienode.NewNodeSet(common.Hash{})
for _, node := range nodes { for _, node := range nodes {
// Hash parameter is not used in pathdb // Hash parameter is not used in pathdb
nodeset.AddNode(node.Path, trienode.New(common.Hash{}, node.SerializedBytes)) nodeset.AddNode(string(node.Path), trienode.New(common.Hash{}, node.SerializedBytes))
} }
// Serialize root commitment form // Serialize root commitment form
return t.Hash(), nodeset return t.Hash(), nodeset