erigontech / erigontech/erigon
execution/state: avoid interface boxing allocations in journal
- Dominant language
- Go
- Stars
- 3.6k
- Forks
- 1.5k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 455
Description
## Problem
The state journal stores entries as `[]journalEntry` where `journalEntry` is an interface:
```go
type journalEntry interface {
revert(s *IntraBlockState) error
dirtied() (accounts.Address, bool)
}
```
Every `journal.append(entry)` call boxes the concrete struct (e.g. `storageChange`, `balanceChange`, `transientStorageChange`) into the interface, which allocates on the heap. In the storage benchmark this accounts for ~3M allocations per run from `storageChange` alone.
## Profile evidence
From `TestBenchmarkEngineXInstruction/storage` memory profile:
```
6357277 6.17% storageChange.revert (alloc on function entry = interface unboxing)
6706824 6.51% SetTransientState → journal.append (transientStorageChange boxing)
4382987 4.26% SetState → journal.append (storageChange boxing)
```
## Possible approaches
1. **Discriminated union**: Replace the interface slice with a struct that holds a type tag + union of all entry types. Eliminates all boxing allocations but requires updating every journal entry type.
2. **Arena allocation**: Use a `sync.Pool`-backed arena to batch-allocate journal entries, reducing per-entry allocation overhead.
3. **Typed slices**: Maintain separate slices per entry type (e.g. `[]storageChange`, `[]balanceChange`) with an ordered index for replay. Avoids interface overhead entirely but complicates revert ordering.
## Context
Found during EVM benchmark profiling in #20183. The journal interface boxing is the largest remaining source of per-opcode heap allocations in storage-heavy workloads.
Contributor guide
Assessment
This issue has not been assessed yet.