mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-27 08:56:18 +00:00
Performance optimizations to the NOMT storage engine while preserving correctness (all triecompare cross-validation tests pass at 10K+ scale): - Pool SHA256 hashers via sync.Pool in HashInternal and HashStem - Replace allStems map with sorted slice + O(N+M) merge (in-place fast path for incremental updates avoids allocation entirely) - Add UpdateSorted to db.DB, skipping redundant sort of pre-sorted ops - Simplify canonicalRoot to use pre-sorted allStems directly - Optimize StemSharedBits with byte-level XOR + bits.LeadingZeros8 - Replace stemLess loops with bytes.Compare in all locations - Eliminate per-stem map alloc in groupAndHashStems (use [256]bool dirty) - Use stack-allocated [248]bool for downBits in BuildInternalTree - Remove unused stemPathCmp function BenchmarkHash/10000/nomt: 9.8ms → 8.2ms (-16%) BenchmarkBlockWorkload/nomt: 7.7ms → 6.6ms (-14%) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
155 lines
4 KiB
Go
155 lines
4 KiB
Go
package nomttrie
|
|
|
|
import (
|
|
"bytes"
|
|
"sort"
|
|
|
|
"github.com/ethereum/go-ethereum/ethdb"
|
|
"github.com/ethereum/go-ethereum/nomt/core"
|
|
)
|
|
|
|
// nomtStemValuePrefix is the ethdb key prefix for stem value flat state.
|
|
// Key format: 0x03 || stem[31] || suffix[1] → value[32]
|
|
const nomtStemValuePrefix byte = 0x03
|
|
|
|
// stemValueDBKey returns the ethdb key for a specific (stem, suffix) value slot.
|
|
func stemValueDBKey(stem core.StemPath, suffix byte) []byte {
|
|
key := make([]byte, 1+core.StemSize+1)
|
|
key[0] = nomtStemValuePrefix
|
|
copy(key[1:], stem[:])
|
|
key[1+core.StemSize] = suffix
|
|
return key
|
|
}
|
|
|
|
// stemValueDBPrefix returns the ethdb prefix for all values of a stem.
|
|
func stemValueDBPrefix(stem core.StemPath) []byte {
|
|
prefix := make([]byte, 1+core.StemSize)
|
|
prefix[0] = nomtStemValuePrefix
|
|
copy(prefix[1:], stem[:])
|
|
return prefix
|
|
}
|
|
|
|
// loadStemValues loads all existing values for a stem from flat state.
|
|
// Uses prefix iteration to efficiently find only populated slots.
|
|
func loadStemValues(diskdb ethdb.Database, stem core.StemPath) ([core.StemNodeWidth][]byte, error) {
|
|
var values [core.StemNodeWidth][]byte
|
|
|
|
prefix := stemValueDBPrefix(stem)
|
|
it := diskdb.NewIterator(prefix, nil)
|
|
defer it.Release()
|
|
|
|
for it.Next() {
|
|
key := it.Key()
|
|
if len(key) != 1+core.StemSize+1 {
|
|
continue
|
|
}
|
|
suffix := key[1+core.StemSize]
|
|
value := make([]byte, len(it.Value()))
|
|
copy(value, it.Value())
|
|
values[suffix] = value
|
|
}
|
|
if err := it.Error(); err != nil {
|
|
return values, err
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
// writeStemValues writes updated stem values to an ethdb batch.
|
|
// Only slots marked dirty are written. Nil values delete the key.
|
|
func writeStemValues(batch ethdb.Batch, stem core.StemPath, values [core.StemNodeWidth][]byte, dirty [core.StemNodeWidth]bool) error {
|
|
for i, d := range dirty {
|
|
if !d {
|
|
continue
|
|
}
|
|
key := stemValueDBKey(stem, byte(i))
|
|
if values[i] == nil {
|
|
if err := batch.Delete(key); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := batch.Put(key, values[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// groupAndHashStems groups stem updates by stem path, loads existing values
|
|
// from flat state, merges updates, computes SHA256 stem hashes, writes updated
|
|
// values back to flat state, and returns sorted StemKeyValue pairs for the
|
|
// NOMT page tree.
|
|
//
|
|
// Returns only stems that have at least one non-nil value (empty stems are
|
|
// excluded, effectively deleting them from the trie).
|
|
func groupAndHashStems(
|
|
updates []stemUpdate,
|
|
diskdb ethdb.Database,
|
|
) ([]core.StemKeyValue, error) {
|
|
if len(updates) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Stable sort by stem then suffix to preserve insertion order for
|
|
// duplicate (stem, suffix) pairs — the last queued value must win.
|
|
sort.SliceStable(updates, func(i, j int) bool {
|
|
if updates[i].Stem != updates[j].Stem {
|
|
return stemLess(&updates[i].Stem, &updates[j].Stem)
|
|
}
|
|
return updates[i].Suffix < updates[j].Suffix
|
|
})
|
|
|
|
batch := diskdb.NewBatch()
|
|
result := make([]core.StemKeyValue, 0, len(updates)/2+1)
|
|
|
|
// Process groups sharing the same stem.
|
|
idx := 0
|
|
for idx < len(updates) {
|
|
stem := updates[idx].Stem
|
|
|
|
// Load existing values.
|
|
values, err := loadStemValues(diskdb, stem)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Apply updates.
|
|
var dirty [core.StemNodeWidth]bool
|
|
for idx < len(updates) && updates[idx].Stem == stem {
|
|
u := updates[idx]
|
|
values[u.Suffix] = u.Value
|
|
dirty[u.Suffix] = true
|
|
idx++
|
|
}
|
|
|
|
// Write to flat state.
|
|
if err := writeStemValues(batch, stem, values, dirty); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Only include stems with at least one non-nil value.
|
|
hasValue := false
|
|
for _, v := range values {
|
|
if v != nil {
|
|
hasValue = true
|
|
break
|
|
}
|
|
}
|
|
if hasValue {
|
|
result = append(result, core.StemKeyValue{
|
|
Stem: stem,
|
|
Hash: core.HashStem(stem, values),
|
|
})
|
|
}
|
|
}
|
|
|
|
if err := batch.Write(); err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// stemLess compares two stem paths lexicographically.
|
|
func stemLess(a, b *core.StemPath) bool {
|
|
return bytes.Compare(a[:], b[:]) < 0
|
|
}
|