Problem
The state journal stores entries as []journalEntry where journalEntry is an interface:
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
-
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.
-
Arena allocation: Use a sync.Pool-backed arena to batch-allocate journal entries, reducing per-entry allocation overhead.
-
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.
Problem
The state journal stores entries as
[]journalEntrywherejournalEntryis an interface: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 fromstorageChangealone.Profile evidence
From
TestBenchmarkEngineXInstruction/storagememory profile:Possible approaches
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.
Arena allocation: Use a
sync.Pool-backed arena to batch-allocate journal entries, reducing per-entry allocation overhead.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.