-
Notifications
You must be signed in to change notification settings - Fork 712
Optimise precompile and create allocations #12105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
|
|
||
| using System.Collections.Frozen; | ||
| using Nethermind.Core; | ||
| using Nethermind.Evm.CodeAnalysis; | ||
| using NUnit.Framework; | ||
|
|
||
| namespace Nethermind.Blockchain.Test; | ||
|
|
||
| [TestFixture] | ||
| [Parallelizable(ParallelScope.All)] | ||
| public class EthereumPrecompileProviderTests | ||
| { | ||
| [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)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,9 +12,8 @@ namespace Nethermind.Blockchain; | |
|
|
||
| public class EthereumPrecompileProvider() : IPrecompileProvider | ||
| { | ||
| private static FrozenDictionary<AddressAsKey, CodeInfo> Precompiles | ||
| { | ||
| get => new Dictionary<AddressAsKey, CodeInfo> | ||
| private static readonly FrozenDictionary<AddressAsKey, CodeInfo> Precompiles = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a genuine bug fix, not just an optimization. The old |
||
| new Dictionary<AddressAsKey, CodeInfo> | ||
| { | ||
| [ECRecoverPrecompile.Address] = new(ECRecoverPrecompile.Instance), | ||
| [Sha256Precompile.Address] = new(Sha256Precompile.Instance), | ||
|
|
@@ -40,7 +39,6 @@ private static FrozenDictionary<AddressAsKey, CodeInfo> Precompiles | |
|
|
||
| [SecP256r1Precompile.Address] = new(SecP256r1Precompile.Instance), | ||
| }.ToFrozenDictionary(); | ||
| } | ||
|
|
||
| public FrozenDictionary<AddressAsKey, CodeInfo> GetPrecompiles() => Precompiles; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -92,6 +92,7 @@ public unsafe partial class VirtualMachine<TGasPolicy>( | |
| private ReadOnlyMemory<byte> _returnDataBuffer = Array.Empty<byte>(); | ||
| protected VmState<TGasPolicy> _currentState; | ||
| protected ReadOnlyMemory<byte>? _previousCallResult; | ||
| protected Address? _previousCallAddressResult; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The invariant is: exactly one of
The optimization is safe: |
||
| protected UInt256 _previousCallOutputDestination; | ||
|
|
||
| public ILogger Logger => _logger; | ||
|
|
@@ -158,6 +159,7 @@ public virtual TransactionSubstate ExecuteTransaction<TTracingInst>( | |
| _codeInfoRepository = TxExecutionContext.CodeInfoRepository; | ||
| _currentState = vmState; | ||
| _previousCallResult = null; | ||
| _previousCallAddressResult = null; | ||
| _previousCallOutputDestination = UInt256.Zero; | ||
| ZeroPaddedSpan previousCallOutput = ZeroPaddedSpan.Empty; | ||
|
|
||
|
|
@@ -204,6 +206,7 @@ public virtual TransactionSubstate ExecuteTransaction<TTracingInst>( | |
| { | ||
| callResult = ExecuteCall<TTracingInst>( | ||
| _previousCallResult, | ||
| _previousCallAddressResult, | ||
| previousCallOutput, | ||
| _previousCallOutputDestination); | ||
| } | ||
|
|
@@ -335,7 +338,8 @@ public TransactionSubstate ExecuteTransaction(VmState<TGasPolicy> vmState, IWorl | |
|
|
||
| protected void PrepareCreateData(VmState<TGasPolicy> previousState, ref ZeroPaddedSpan previousCallOutput) | ||
| { | ||
| _previousCallResult = previousState.Env.ExecutingAccount.Bytes.ToArray(); | ||
| _previousCallResult = null; | ||
| _previousCallAddressResult = previousState.Env.ExecutingAccount; | ||
| _previousCallOutputDestination = UInt256.Zero; | ||
| ReturnDataBuffer = Array.Empty<byte>(); | ||
| previousCallOutput = ZeroPaddedSpan.Empty; | ||
|
|
@@ -346,6 +350,7 @@ protected ZeroPaddedSpan HandleRegularReturn<TTracingInst>(scoped in CallResult | |
| { | ||
| ZeroPaddedSpan previousCallOutput; | ||
| ReturnDataBuffer = callResult.Output; | ||
| _previousCallAddressResult = null; | ||
| _previousCallResult = callResult.PrecompileSuccess.HasValue | ||
| ? (callResult.PrecompileSuccess.Value ? StatusCode.SuccessBytes : StatusCode.FailureBytes) | ||
| : StatusCode.SuccessBytes; | ||
|
|
@@ -470,6 +475,7 @@ private void TryChargeAndDepositCode( | |
| } | ||
|
|
||
| _previousCallResult = BytesZero; | ||
| _previousCallAddressResult = null; | ||
| previousStateSucceeded = false; | ||
|
|
||
| if (_txTracer.IsTracingActions) | ||
|
|
@@ -512,6 +518,7 @@ protected void HandleRevert(VmState<TGasPolicy> previousState, in CallResult cal | |
| ReturnDataBuffer = outputBytes; | ||
|
|
||
| _previousCallResult = StatusCode.FailureBytes; | ||
| _previousCallAddressResult = null; | ||
|
|
||
| // Slice the output bytes, zero-padding if necessary, to match the expected output length. | ||
| // This ensures that the returned data conforms to the caller's output length expectations. | ||
|
|
@@ -590,6 +597,7 @@ protected TransactionSubstate HandleFailure<TTracingInst>(Exception failure, str | |
| } | ||
|
|
||
| _previousCallResult = StatusCode.FailureBytes; | ||
| _previousCallAddressResult = null; | ||
| bool failedCreate = _currentState.ExecutionType.IsAnyCreate(); | ||
|
|
||
| // Reset output destination and return data. | ||
|
|
@@ -722,6 +730,7 @@ protected void PrepareNextCallFrame(in CallResult callResult, ref ZeroPaddedSpan | |
|
|
||
| // Clear the previous call result as the execution context is moving to a new frame. | ||
| _previousCallResult = null; | ||
| _previousCallAddressResult = null; | ||
|
|
||
| // Reset the return data buffer to ensure no residual data persists across call frames. | ||
| ReturnDataBuffer = Array.Empty<byte>(); | ||
|
|
@@ -772,6 +781,7 @@ protected TransactionSubstate HandleException(scoped in CallResult callResult, s | |
| } | ||
|
|
||
| _previousCallResult = StatusCode.FailureBytes; | ||
| _previousCallAddressResult = null; | ||
| bool failedCreate = _currentState.ExecutionType.IsAnyCreate(); | ||
|
|
||
| // Reset output destination and clear return data. | ||
|
|
@@ -1091,6 +1101,9 @@ protected static string GetErrorString(IPrecompile precompile, string? error) | |
| /// An optional read-only memory buffer containing the output of a previous call; if provided, its bytes | ||
| /// will be pushed onto the stack for further processing. | ||
| /// </param> | ||
| /// <param name="previousCallAddressResult"> | ||
| /// An optional contract creation result address to push onto the stack. | ||
| /// </param> | ||
| /// <param name="previousCallOutput"> | ||
| /// A zero-padded span containing output from the previous call used for updating the memory state. | ||
| /// </param> | ||
|
|
@@ -1108,6 +1121,7 @@ protected static string GetErrorString(IPrecompile precompile, string? error) | |
| [SkipLocalsInit] | ||
| protected CallResult ExecuteCall<TTracingInst>( | ||
| ReadOnlyMemory<byte>? previousCallResult, | ||
| Address? previousCallAddressResult, | ||
| ZeroPaddedSpan previousCallOutput, | ||
| scoped in UInt256 previousCallOutputDestination) | ||
| where TTracingInst : struct, IFlag | ||
|
|
@@ -1157,8 +1171,19 @@ protected CallResult ExecuteCall<TTracingInst>( | |
| // gas/state-gas accounting without needing interpreter-wide exception handling. | ||
| ref TGasPolicy gas = ref vmState.Gas; | ||
|
|
||
| // If a previous call result exists, push its bytes onto the stack. | ||
| if (previousCallResult is not null) | ||
| // If a previous call or create result exists, push its bytes onto the stack. | ||
| if (previousCallAddressResult is not null) | ||
| { | ||
| EvmExceptionType pushResult = stack.PushBytes<TTracingInst>(previousCallAddressResult.Bytes); | ||
| if (pushResult != EvmExceptionType.None) return new(pushResult); | ||
|
|
||
| // Report the remaining gas if tracing instructions are enabled. | ||
| if (TTracingInst.IsActive) | ||
| { | ||
| _txTracer.ReportOperationRemainingGas(TGasPolicy.GetRemainingGas(vmState.Gas)); | ||
| } | ||
| } | ||
| else if (previousCallResult is not null) | ||
| { | ||
| EvmExceptionType pushResult = stack.PushBytes<TTracingInst>(previousCallResult.Value.Span); | ||
| if (pushResult != EvmExceptionType.None) return new(pushResult); | ||
|
|
||
There was a problem hiding this comment.
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
EthereumPrecompileProviderinstances return the same reference — that would demonstrate thestaticpart of the fix (not just repeated calls on the same instance):