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

View file

@ -309,7 +309,10 @@ func (t *BinaryTrie) Hash() common.Hash {
func (t *BinaryTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) {
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)))
})
if err != nil {