2ce00adb55
* focus on performance improvement in many aspects. 1. Do BlockBody verification concurrently; 2. Do calculation of intermediate root concurrently; 3. Preload accounts before processing blocks; 4. Make the snapshot layers configurable. 5. Reuse some object to reduce GC. add * rlp: improve decoder stream implementation (#22858) This commit makes various cleanup changes to rlp.Stream. * rlp: shrink Stream struct This removes a lot of unused padding space in Stream by reordering the fields. The size of Stream changes from 120 bytes to 88 bytes. Stream instances are internally cached and reused using sync.Pool, so this does not improve performance. * rlp: simplify list stack The list stack kept track of the size of the current list context as well as the current offset into it. The size had to be stored in the stack in order to subtract it from the remaining bytes of any enclosing list in ListEnd. It seems that this can be implemented in a simpler way: just subtract the size from the enclosing list context in List instead. * rlp: use atomic.Value for type cache (#22902) All encoding/decoding operations read the type cache to find the writer/decoder function responsible for a type. When analyzing CPU profiles of geth during sync, I found that the use of sync.RWMutex in cache lookups appears in the profiles. It seems we are running into CPU cache contention problems when package rlp is heavily used on all CPU cores during sync. This change makes it use atomic.Value + a writer lock instead of sync.RWMutex. In the common case where the typeinfo entry is present in the cache, we simply fetch the map and lookup the type. * rlp: optimize byte array handling (#22924) This change improves the performance of encoding/decoding [N]byte. name old time/op new time/op delta DecodeByteArrayStruct-8 336ns ± 0% 246ns ± 0% -26.98% (p=0.000 n=9+10) EncodeByteArrayStruct-8 225ns ± 1% 148ns ± 1% -34.12% (p=0.000 n=10+10) name old alloc/op new alloc/op delta DecodeByteArrayStruct-8 120B ± 0% 48B ± 0% -60.00% (p=0.000 n=10+10) EncodeByteArrayStruct-8 0.00B 0.00B ~ (all equal) * rlp: optimize big.Int decoding for size <= 32 bytes (#22927) This change grows the static integer buffer in Stream to 32 bytes, making it possible to decode 256bit integers without allocating a temporary buffer. In the recent commit 088da24, Stream struct size decreased from 120 bytes down to 88 bytes. This commit grows the struct to 112 bytes again, but the size change will not degrade performance because Stream instances are internally cached in sync.Pool. name old time/op new time/op delta DecodeBigInts-8 12.2µs ± 0% 8.6µs ± 4% -29.58% (p=0.000 n=9+10) name old speed new speed delta DecodeBigInts-8 230MB/s ± 0% 326MB/s ± 4% +42.04% (p=0.000 n=9+10) * eth/protocols/eth, les: avoid Raw() when decoding HashOrNumber (#22841) Getting the raw value is not necessary to decode this type, and decoding it directly from the stream is faster. * fix testcase * debug no lazy * fix can not repair * address comments Co-authored-by: Felix Lange <fjl@twurst.com>
272 lines
7.7 KiB
Go
272 lines
7.7 KiB
Go
// Copyright 2019 The go-ethereum Authors
|
|
// This file is part of the go-ethereum library.
|
|
//
|
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package trie
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
"golang.org/x/crypto/sha3"
|
|
)
|
|
|
|
// leafChanSize is the size of the leafCh. It's a pretty arbitrary number, to allow
|
|
// some parallelism but not incur too much memory overhead.
|
|
const leafChanSize = 200
|
|
|
|
// leaf represents a trie leaf value
|
|
type leaf struct {
|
|
size int // size of the rlp data (estimate)
|
|
hash common.Hash // hash of rlp data
|
|
node node // the node to commit
|
|
}
|
|
|
|
// committer is a type used for the trie Commit operation. A committer has some
|
|
// internal preallocated temp space, and also a callback that is invoked when
|
|
// leaves are committed. The leafs are passed through the `leafCh`, to allow
|
|
// some level of parallelism.
|
|
// By 'some level' of parallelism, it's still the case that all leaves will be
|
|
// processed sequentially - onleaf will never be called in parallel or out of order.
|
|
type committer struct {
|
|
sha crypto.KeccakState
|
|
|
|
onleaf LeafCallback
|
|
leafCh chan *leaf
|
|
}
|
|
|
|
// committers live in a global sync.Pool
|
|
var committerPool = sync.Pool{
|
|
New: func() interface{} {
|
|
return &committer{
|
|
sha: sha3.NewLegacyKeccak256().(crypto.KeccakState),
|
|
}
|
|
},
|
|
}
|
|
|
|
// newCommitter creates a new committer or picks one from the pool.
|
|
func newCommitter() *committer {
|
|
return committerPool.Get().(*committer)
|
|
}
|
|
|
|
func returnCommitterToPool(h *committer) {
|
|
h.onleaf = nil
|
|
h.leafCh = nil
|
|
committerPool.Put(h)
|
|
}
|
|
|
|
// commit collapses a node down into a hash node and inserts it into the database
|
|
func (c *committer) Commit(n node, db *Database) (hashNode, error) {
|
|
if db == nil {
|
|
return nil, errors.New("no db provided")
|
|
}
|
|
h, err := c.commit(n, db)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return h.(hashNode), nil
|
|
}
|
|
|
|
// commit collapses a node down into a hash node and inserts it into the database
|
|
func (c *committer) commit(n node, db *Database) (node, error) {
|
|
// if this path is clean, use available cached data
|
|
hash, dirty := n.cache()
|
|
if hash != nil && !dirty {
|
|
return hash, nil
|
|
}
|
|
// Commit children, then parent, and remove remove the dirty flag.
|
|
switch cn := n.(type) {
|
|
case *shortNode:
|
|
// Commit child
|
|
cn.flags.dirty = false
|
|
collapsed := cn.copy()
|
|
|
|
// If the child is fullnode, recursively commit.
|
|
// Otherwise it can only be hashNode or valueNode.
|
|
if _, ok := cn.Val.(*fullNode); ok {
|
|
childV, err := c.commit(cn.Val, db)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
collapsed.Val = childV
|
|
}
|
|
// The key needs to be copied, since we're delivering it to database
|
|
collapsed.Key = hexToCompact(cn.Key)
|
|
hashedNode := c.store(collapsed, db)
|
|
if hn, ok := hashedNode.(hashNode); ok {
|
|
return hn, nil
|
|
}
|
|
return collapsed, nil
|
|
case *fullNode:
|
|
cn.flags.dirty = false
|
|
hashedKids, err := c.commitChildren(cn, db)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
collapsed := cn.copy()
|
|
collapsed.Children = hashedKids
|
|
|
|
hashedNode := c.store(collapsed, db)
|
|
if hn, ok := hashedNode.(hashNode); ok {
|
|
return hn, nil
|
|
}
|
|
return collapsed, nil
|
|
case hashNode:
|
|
return cn, nil
|
|
default:
|
|
// nil, valuenode shouldn't be committed
|
|
panic(fmt.Sprintf("%T: invalid node: %v", n, n))
|
|
}
|
|
}
|
|
|
|
// commitChildren commits the children of the given fullnode
|
|
func (c *committer) commitChildren(n *fullNode, db *Database) ([17]node, error) {
|
|
var children [17]node
|
|
for i := 0; i < 16; i++ {
|
|
child := n.Children[i]
|
|
if child == nil {
|
|
continue
|
|
}
|
|
// If it's the hashed child, save the hash value directly.
|
|
// Note: it's impossible that the child in range [0, 15]
|
|
// is a valuenode.
|
|
if hn, ok := child.(hashNode); ok {
|
|
children[i] = hn
|
|
continue
|
|
}
|
|
// Commit the child recursively and store the "hashed" value.
|
|
// Note the returned node can be some embedded nodes, so it's
|
|
// possible the type is not hashnode.
|
|
hashed, err := c.commit(child, db)
|
|
if err != nil {
|
|
return children, err
|
|
}
|
|
children[i] = hashed
|
|
}
|
|
// For the 17th child, it's possible the type is valuenode.
|
|
if n.Children[16] != nil {
|
|
children[16] = n.Children[16]
|
|
}
|
|
return children, nil
|
|
}
|
|
|
|
// store hashes the node n and if we have a storage layer specified, it writes
|
|
// the key/value pair to it and tracks any node->child references as well as any
|
|
// node->external trie references.
|
|
func (c *committer) store(n node, db *Database) node {
|
|
// Larger nodes are replaced by their hash and stored in the database.
|
|
var (
|
|
hash, _ = n.cache()
|
|
size int
|
|
)
|
|
if hash == nil {
|
|
// This was not generated - must be a small node stored in the parent.
|
|
// In theory we should apply the leafCall here if it's not nil(embedded
|
|
// node usually contains value). But small value(less than 32bytes) is
|
|
// not our target.
|
|
return n
|
|
} else {
|
|
// We have the hash already, estimate the RLP encoding-size of the node.
|
|
// The size is used for mem tracking, does not need to be exact
|
|
size = estimateSize(n)
|
|
}
|
|
// If we're using channel-based leaf-reporting, send to channel.
|
|
// The leaf channel will be active only when there an active leaf-callback
|
|
if c.leafCh != nil {
|
|
c.leafCh <- &leaf{
|
|
size: size,
|
|
hash: common.BytesToHash(hash),
|
|
node: n,
|
|
}
|
|
} else if db != nil {
|
|
// No leaf-callback used, but there's still a database. Do serial
|
|
// insertion
|
|
db.lock.Lock()
|
|
db.insert(common.BytesToHash(hash), size, n)
|
|
db.lock.Unlock()
|
|
}
|
|
return hash
|
|
}
|
|
|
|
// commitLoop does the actual insert + leaf callback for nodes.
|
|
func (c *committer) commitLoop(db *Database) {
|
|
for item := range c.leafCh {
|
|
var (
|
|
hash = item.hash
|
|
size = item.size
|
|
n = item.node
|
|
)
|
|
// We are pooling the trie nodes into an intermediate memory cache
|
|
db.lock.Lock()
|
|
db.insert(hash, size, n)
|
|
db.lock.Unlock()
|
|
|
|
if c.onleaf != nil {
|
|
switch n := n.(type) {
|
|
case *shortNode:
|
|
if child, ok := n.Val.(valueNode); ok {
|
|
c.onleaf(nil, nil, child, hash)
|
|
}
|
|
case *fullNode:
|
|
// For children in range [0, 15], it's impossible
|
|
// to contain valuenode. Only check the 17th child.
|
|
if n.Children[16] != nil {
|
|
c.onleaf(nil, nil, n.Children[16].(valueNode), hash)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *committer) makeHashNode(data []byte) hashNode {
|
|
n := make(hashNode, c.sha.Size())
|
|
c.sha.Reset()
|
|
c.sha.Write(data)
|
|
c.sha.Read(n)
|
|
return n
|
|
}
|
|
|
|
// estimateSize estimates the size of an rlp-encoded node, without actually
|
|
// rlp-encoding it (zero allocs). This method has been experimentally tried, and with a trie
|
|
// with 1000 leafs, the only errors above 1% are on small shortnodes, where this
|
|
// method overestimates by 2 or 3 bytes (e.g. 37 instead of 35)
|
|
func estimateSize(n node) int {
|
|
switch n := n.(type) {
|
|
case *shortNode:
|
|
// A short node contains a compacted key, and a value.
|
|
return 3 + len(n.Key) + estimateSize(n.Val)
|
|
case *fullNode:
|
|
// A full node contains up to 16 hashes (some nils), and a key
|
|
s := 3
|
|
for i := 0; i < 16; i++ {
|
|
if child := n.Children[i]; child != nil {
|
|
s += estimateSize(child)
|
|
} else {
|
|
s++
|
|
}
|
|
}
|
|
return s
|
|
case valueNode:
|
|
return 1 + len(n)
|
|
case hashNode:
|
|
return 1 + len(n)
|
|
default:
|
|
panic(fmt.Sprintf("node type %T", n))
|
|
}
|
|
}
|