-
Notifications
You must be signed in to change notification settings - Fork 21.9k
trie: reduce the memory allocation in trie hashing #31902
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a80f906
4c85ff8
e7e19e7
f64a89f
d5463bb
b7aa034
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,8 @@ | |
| package trie | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "fmt" | ||
| "sync" | ||
|
|
||
| "github.com/ethereum/go-ethereum/crypto" | ||
|
|
@@ -54,109 +56,118 @@ func returnHasherToPool(h *hasher) { | |
| } | ||
|
|
||
| // hash collapses a node down into a hash node. | ||
| func (h *hasher) hash(n node, force bool) node { | ||
| func (h *hasher) hash(n node, force bool) []byte { | ||
| // Return the cached hash if it's available | ||
| if hash, _ := n.cache(); hash != nil { | ||
| return hash | ||
| } | ||
| // Trie not processed yet, walk the children | ||
| switch n := n.(type) { | ||
| case *shortNode: | ||
| collapsed := h.hashShortNodeChildren(n) | ||
| hashed := h.shortnodeToHash(collapsed, force) | ||
| if hn, ok := hashed.(hashNode); ok { | ||
| n.flags.hash = hn | ||
| } else { | ||
| n.flags.hash = nil | ||
| enc := h.encodeShortNode(n) | ||
| if len(enc) < 32 && !force { | ||
| // Nodes smaller than 32 bytes are embedded directly in their parent. | ||
| // In such cases, return the raw encoded blob instead of the node hash. | ||
| // It's essential to deep-copy the node blob, as the underlying buffer | ||
| // of enc will be reused later. | ||
| buf := make([]byte, len(enc)) | ||
| copy(buf, enc) | ||
| return buf | ||
| } | ||
| return hashed | ||
| hash := h.hashData(enc) | ||
| n.flags.hash = hash | ||
| return hash | ||
|
|
||
| case *fullNode: | ||
| collapsed := h.hashFullNodeChildren(n) | ||
| hashed := h.fullnodeToHash(collapsed, force) | ||
| if hn, ok := hashed.(hashNode); ok { | ||
| n.flags.hash = hn | ||
| } else { | ||
| n.flags.hash = nil | ||
| enc := h.encodeFullNode(n) | ||
| if len(enc) < 32 && !force { | ||
| // Nodes smaller than 32 bytes are embedded directly in their parent. | ||
| // In such cases, return the raw encoded blob instead of the node hash. | ||
| // It's essential to deep-copy the node blob, as the underlying buffer | ||
| // of enc will be reused later. | ||
| buf := make([]byte, len(enc)) | ||
| copy(buf, enc) | ||
| return buf | ||
| } | ||
| return hashed | ||
| default: | ||
| // Value and hash nodes don't have children, so they're left as were | ||
| hash := h.hashData(enc) | ||
| n.flags.hash = hash | ||
| return hash | ||
|
|
||
| case hashNode: | ||
| // hash nodes don't have children, so they're left as were | ||
| return n | ||
|
|
||
| default: | ||
| panic(fmt.Errorf("unexpected node type, %T", n)) | ||
| } | ||
| } | ||
|
|
||
| // hashShortNodeChildren returns a copy of the supplied shortNode, with its child | ||
| // being replaced by either the hash or an embedded node if the child is small. | ||
| func (h *hasher) hashShortNodeChildren(n *shortNode) *shortNode { | ||
| var collapsed shortNode | ||
| collapsed.Key = hexToCompact(n.Key) | ||
| switch n.Val.(type) { | ||
| case *fullNode, *shortNode: | ||
| collapsed.Val = h.hash(n.Val, false) | ||
| default: | ||
| collapsed.Val = n.Val | ||
| // encodeShortNode encodes the provided shortNode into the bytes. Notably, the | ||
| // return slice must be deep-copied explicitly, otherwise the underlying slice | ||
| // will be reused later. | ||
| func (h *hasher) encodeShortNode(n *shortNode) []byte { | ||
| // Encode leaf node | ||
| if hasTerm(n.Key) { | ||
| var ln leafNodeEncoder | ||
| ln.Key = hexToCompact(n.Key) | ||
| ln.Val = n.Val.(valueNode) | ||
| ln.encode(h.encbuf) | ||
| return h.encodedBytes() | ||
| } | ||
| return &collapsed | ||
| // Encode extension node | ||
| var en extNodeEncoder | ||
| en.Key = hexToCompact(n.Key) | ||
| en.Val = h.hash(n.Val, false) | ||
| en.encode(h.encbuf) | ||
| return h.encodedBytes() | ||
| } | ||
|
|
||
| // fnEncoderPool is the pool for storing shared fullNode encoder to mitigate | ||
| // the significant memory allocation overhead. | ||
| var fnEncoderPool = sync.Pool{ | ||
| New: func() interface{} { | ||
| var enc fullnodeEncoder | ||
| return &enc | ||
| }, | ||
| } | ||
|
|
||
| // hashFullNodeChildren returns a copy of the supplied fullNode, with its child | ||
| // being replaced by either the hash or an embedded node if the child is small. | ||
| func (h *hasher) hashFullNodeChildren(n *fullNode) *fullNode { | ||
| var children [17]node | ||
| // encodeFullNode encodes the provided fullNode into the bytes. Notably, the | ||
| // return slice must be deep-copied explicitly, otherwise the underlying slice | ||
| // will be reused later. | ||
| func (h *hasher) encodeFullNode(n *fullNode) []byte { | ||
| fn := fnEncoderPool.Get().(*fullnodeEncoder) | ||
| fn.reset() | ||
|
|
||
| if h.parallel { | ||
| var wg sync.WaitGroup | ||
| for i := 0; i < 16; i++ { | ||
| if child := n.Children[i]; child != nil { | ||
| wg.Add(1) | ||
| go func(i int) { | ||
| hasher := newHasher(false) | ||
| children[i] = hasher.hash(child, false) | ||
| returnHasherToPool(hasher) | ||
| wg.Done() | ||
| }(i) | ||
| } else { | ||
| children[i] = nilValueNode | ||
| if n.Children[i] == nil { | ||
| continue | ||
| } | ||
| wg.Add(1) | ||
| go func(i int) { | ||
| defer wg.Done() | ||
|
|
||
| h := newHasher(false) | ||
| fn.Children[i] = h.hash(n.Children[i], false) | ||
| returnHasherToPool(h) | ||
| }(i) | ||
| } | ||
| wg.Wait() | ||
| } else { | ||
| for i := 0; i < 16; i++ { | ||
| if child := n.Children[i]; child != nil { | ||
| children[i] = h.hash(child, false) | ||
| } else { | ||
| children[i] = nilValueNode | ||
| fn.Children[i] = h.hash(child, false) | ||
| } | ||
| } | ||
| } | ||
| if n.Children[16] != nil { | ||
| children[16] = n.Children[16] | ||
| } | ||
| return &fullNode{flags: nodeFlag{}, Children: children} | ||
| } | ||
|
|
||
| // shortNodeToHash computes the hash of the given shortNode. The shortNode must | ||
| // first be collapsed, with its key converted to compact form. If the RLP-encoded | ||
| // node data is smaller than 32 bytes, the node itself is returned. | ||
| func (h *hasher) shortnodeToHash(n *shortNode, force bool) node { | ||
| n.encode(h.encbuf) | ||
| enc := h.encodedBytes() | ||
|
|
||
| if len(enc) < 32 && !force { | ||
| return n // Nodes smaller than 32 bytes are stored inside their parent | ||
| fn.Children[16] = n.Children[16].(valueNode) | ||
| } | ||
| return h.hashData(enc) | ||
| } | ||
|
|
||
| // fullnodeToHash computes the hash of the given fullNode. If the RLP-encoded | ||
| // node data is smaller than 32 bytes, the node itself is returned. | ||
| func (h *hasher) fullnodeToHash(n *fullNode, force bool) node { | ||
| n.encode(h.encbuf) | ||
| enc := h.encodedBytes() | ||
| fn.encode(h.encbuf) | ||
| fnEncoderPool.Put(fn) | ||
|
|
||
| if len(enc) < 32 && !force { | ||
| return n // Nodes smaller than 32 bytes are stored inside their parent | ||
| } | ||
| return h.hashData(enc) | ||
| return h.encodedBytes() | ||
| } | ||
|
|
||
| // encodedBytes returns the result of the last encoding operation on h.encbuf. | ||
|
|
@@ -175,9 +186,10 @@ func (h *hasher) encodedBytes() []byte { | |
| return h.tmp | ||
| } | ||
|
|
||
| // hashData hashes the provided data | ||
| func (h *hasher) hashData(data []byte) hashNode { | ||
| n := make(hashNode, 32) | ||
| // hashData hashes the provided data. It is safe to modify the returned slice after | ||
| // the function returns. | ||
| func (h *hasher) hashData(data []byte) []byte { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we have made the return type here and
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds reasonable to me. But I don't want to do it in this pr, otherwise it will end up as a giant PR I think. |
||
| n := make([]byte, 32) | ||
| h.sha.Reset() | ||
| h.sha.Write(data) | ||
| h.sha.Read(n) | ||
|
|
@@ -192,20 +204,17 @@ func (h *hasher) hashDataTo(dst, data []byte) { | |
| h.sha.Read(dst) | ||
| } | ||
|
|
||
| // proofHash is used to construct trie proofs, and returns the 'collapsed' | ||
| // node (for later RLP encoding) as well as the hashed node -- unless the | ||
| // node is smaller than 32 bytes, in which case it will be returned as is. | ||
| // This method does not do anything on value- or hash-nodes. | ||
| func (h *hasher) proofHash(original node) (collapsed, hashed node) { | ||
| // proofHash is used to construct trie proofs, returning the rlp-encoded node blobs. | ||
| // Note, only resolved node (shortNode or fullNode) is expected for proofing. | ||
| // | ||
| // It is safe to modify the returned slice after the function returns. | ||
| func (h *hasher) proofHash(original node) []byte { | ||
| switch n := original.(type) { | ||
| case *shortNode: | ||
| sn := h.hashShortNodeChildren(n) | ||
| return sn, h.shortnodeToHash(sn, false) | ||
| return bytes.Clone(h.encodeShortNode(n)) | ||
| case *fullNode: | ||
| fn := h.hashFullNodeChildren(n) | ||
| return fn, h.fullnodeToHash(fn, false) | ||
| return bytes.Clone(h.encodeFullNode(n)) | ||
| default: | ||
| // Value and hash nodes don't have children, so they're left as were | ||
| return n, n | ||
| panic(fmt.Errorf("unexpected node type, %T", original)) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this introduces a synchronization point for all parallel hashers, which is probably why the performance degraded slightly. I think we can do better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really? I think sync.Pool should be efficient for concurrent usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, it might be efficient in what it's doing but it is certainly not free.