erigontech / erigontech/erigon

execution/state: avoid interface boxing allocations in journal

Open
#20,186 2 comments 0 reactions 1 assignee Claimed by @yperbasis View on GitHub
performance
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.