Skip to content

Optimise precompile and create allocations#12105

Draft
flcl42 wants to merge 1 commit into
masterfrom
perf/precompile-create-alloc
Draft

Optimise precompile and create allocations#12105
flcl42 wants to merge 1 commit into
masterfrom
perf/precompile-create-alloc

Conversation

@flcl42

@flcl42 flcl42 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Fixes Closes Resolves #

Please choose one of the keywords above to refer to the issue this PR solves followed by the issue number (e.g. Fixes #000). If no issue number, remove the line. Also, remove everything marked optional that is not applicable. Remove this note after reading.

Changes

  • List the changes

Types of changes

What types of changes does your code introduce?

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (a change that causes existing functionality not to work as expected)
  • Optimization
  • Refactoring
  • Documentation update
  • Build-related changes
  • Other: Description

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

@claude

claude Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Claude finished @flcl42's task in 5m 11s —— View job


PR Review: Optimise precompile and create allocations

  • Gather context and load relevant rules
  • Read changed files and diff
  • Analyze changes for correctness, performance, and style
  • Post review with inline comments

Summary

Two independent, correct optimizations. No critical or high severity findings.

EthereumPrecompileProvider.cs — Bug fix + optimization

The old Precompiles property was a getter that called ToFrozenDictionary() on a freshly-constructed Dictionary on every single call, allocating ~20 CodeInfo wrappers and a full frozen dictionary each time. The static readonly field is initialized once at class load time, is thread-safe under CLR static-constructor guarantees, and is safe to share because FrozenDictionary and precompile CodeInfo objects are effectively immutable. This is a correctness/performance bug fix.

Callers verified: CodeInfoRepository (stores in a field — safe), TaikoPrecompileProvider and OptimismPrecompileProvider (copy entries into a new local dict — safe), PrecompileCachedCodeInfoRepository (reads entries — safe).

VirtualMachine.cs — Avoids byte[] allocation per CREATE

The old PrepareCreateData called .Bytes.ToArray() to allocate a 20-byte heap array that was only needed to push the newly-created contract address onto the EVM stack. The new code stores the Address reference directly (immutable, private readonly ValueAddress backing store) and uses .Bytes (a ReadOnlySpan<byte>) at push time — same 20-byte span, zero allocation.

The mutual exclusion invariant between _previousCallAddressResult and _previousCallResult is correctly maintained across all state transition paths (verified all call sites).


Findings

Severity Finding
Low Test only verifies same-instance caching; a cross-instance assertion would strengthen coverage of the static invariant (inline suggestion added)
Low PR description body still contains the template placeholder — worth filling in for traceability

No blocking issues. The logic is sound and the optimizations are correct.

private static FrozenDictionary<AddressAsKey, CodeInfo> Precompiles
{
get => new Dictionary<AddressAsKey, CodeInfo>
private static readonly FrozenDictionary<AddressAsKey, CodeInfo> Precompiles =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a genuine bug fix, not just an optimization. The old get-only property was calling ToFrozenDictionary() on a freshly-constructed Dictionary on every call to GetPrecompiles(), allocating ~20 CodeInfo wrappers and building a new frozen dictionary each time. The static readonly field initializer runs exactly once at class initialization — correct and thread-safe under CLR static-init guarantees.

private ReadOnlyMemory<byte> _returnDataBuffer = Array.Empty<byte>();
protected VmState<TGasPolicy> _currentState;
protected ReadOnlyMemory<byte>? _previousCallResult;
protected Address? _previousCallAddressResult;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The invariant is: exactly one of _previousCallAddressResult and _previousCallResult is non-null when a value needs to be pushed, never both. This is correctly maintained across all code paths:

  • PrepareCreateData (success): sets address non-null, result null
  • TryChargeAndDepositCode failure: sets result=BytesZero, address=null ✓
  • HandleRevert: sets result=FailureBytes, address=null ✓
  • HandleFailure: sets result=FailureBytes, address=null ✓
  • PrepareNextCallFrame: clears both to null ✓
  • HandleRegularReturn: sets result=status, address=null ✓

The optimization is safe: Address.Bytes returns a ReadOnlySpan<byte> over an immutable private readonly ValueAddress, so no defensive copy is needed. The pushed 20-byte span is identical to what previousState.Env.ExecutingAccount.Bytes.ToArray() produced before.

FrozenDictionary<AddressAsKey, CodeInfo> precompilesAgain = provider.GetPrecompiles();

Assert.That(precompilesAgain, Is.SameAs(precompiles));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider also asserting that two different EthereumPrecompileProvider instances return the same reference — that would demonstrate the static part of the fix (not just repeated calls on the same instance):

Suggested change
}
[Test]
public void GetPrecompiles_ReturnsCachedDictionary()
{
EthereumPrecompileProvider provider = new();
FrozenDictionary<AddressAsKey, CodeInfo> precompiles = provider.GetPrecompiles();
FrozenDictionary<AddressAsKey, CodeInfo> precompilesAgain = provider.GetPrecompiles();
Assert.That(precompilesAgain, Is.SameAs(precompiles));
}
[Test]
public void GetPrecompiles_ReturnsSameReferenceAcrossInstances()
{
EthereumPrecompileProvider first = new();
EthereumPrecompileProvider second = new();
Assert.That(second.GetPrecompiles(), Is.SameAs(first.GetPrecompiles()));
}

@github-actions

Copy link
Copy Markdown
Contributor

EVM Opcode Benchmark Diff

Aggregated runs: base=3, pr=3
Noisy rerun opcodes: MCOPY

Regressions (1)

Opcode Base Median (ns) PR Median (ns) Delta Abs Δ (ns) Base CV PR CV Threshold Uncertainty Effective
SELFBALANCE 212.820 235.260 +10.54% 22.440 1.6% 2.0% ±5.0% ±2.8% ±5.0%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant