Skip to content

execution/state: avoid interface boxing allocations in journal #20186

Description

@yperbasis

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

  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.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions