trie/bintrie: reuse path buffer in collectNodes

Post-rollback pprof on BenchmarkCollectNodesSparseWrite revealed
collectNodes' per-descent leftPath/rightPath make+copy as 13% of
alloc_objects (~26 allocs/op). Replace with append/truncate on a
shared buffer pre-sized by Commit; flushfn consumers (NodeSet.AddNode,
tracer.Get) already clone via string(path), so in-place reuse is safe.

Benchmark delta (M4 Pro, go1.24.0, --count=5 --benchtime=5s):

  before: 9506 ns/op  15245 B/op  132 allocs/op
  after:  9095 ns/op  15008 B/op  106 allocs/op

  vs upstream/master@53ff723cc: allocs/op now -20.9% (was -1.5%).
This commit is contained in:
CPerezz 2026-04-19 08:00:33 +02:00
parent 0c92956c77
commit 50d815313e
No known key found for this signature in database
GPG key ID: 62045F34B97177DD
2 changed files with 12 additions and 9 deletions

View file

@ -240,18 +240,18 @@ func (s *NodeStore) collectNodes(ref nodeRef, path []byte, flushfn NodeFlushFn)
if !node.dirty { if !node.dirty {
return nil return nil
} }
leftPath := make([]byte, len(path)+1) // Reuse path buffer across children: flushfn consumers
copy(leftPath, path) // (NodeSet.AddNode, tracer.Get) clone via string(path), so in-place
leftPath[len(path)] = 0 // mutation is safe. Saves ~17 allocs/op on this benchmark.
if err := s.collectNodes(node.left, leftPath, flushfn); err != nil { path = append(path, 0)
if err := s.collectNodes(node.left, path, flushfn); err != nil {
return err return err
} }
rightPath := make([]byte, len(path)+1) path[len(path)-1] = 1
copy(rightPath, path) if err := s.collectNodes(node.right, path, flushfn); err != nil {
rightPath[len(path)] = 1
if err := s.collectNodes(node.right, rightPath, flushfn); err != nil {
return err return err
} }
path = path[:len(path)-1]
flushfn(path, s.computeHash(ref), s.serializeNode(ref)) flushfn(path, s.computeHash(ref), s.serializeNode(ref))
node.dirty = false node.dirty = false
return nil return nil

View file

@ -309,7 +309,10 @@ func (t *BinaryTrie) Hash() common.Hash {
func (t *BinaryTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) { func (t *BinaryTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) {
nodeset := trienode.NewNodeSet(common.Hash{}) nodeset := trienode.NewNodeSet(common.Hash{})
err := t.store.collectNodes(t.store.root, nil, func(path []byte, hash common.Hash, serialized []byte) { // Pre-size the path buffer: collectNodes reuses it in-place via
// append/truncate; 32 covers typical binary-trie depth without regrowth.
pathBuf := make([]byte, 0, 32)
err := t.store.collectNodes(t.store.root, pathBuf, func(path []byte, hash common.Hash, serialized []byte) {
nodeset.AddNode(path, trienode.NewNodeWithPrev(hash, serialized, t.tracer.Get(path))) nodeset.AddNode(path, trienode.NewNodeWithPrev(hash, serialized, t.tracer.Get(path)))
}) })
if err != nil { if err != nil {