Skip to content

Skip multi-dimensional gas accounting for eth_call simulations (~10% latency improvement) #4560

@cristian-dutu

Description

@cristian-dutu

Is your feature request related to a problem? Please describe.

eth_call simulations spend ~8.4% of total CPU time on multi-dimensional gas accounting (addConstantMultiGas + MultiGas.SaturatingAddInto) that is never consumed. The UsedMultiGas struct tracks gas across 7 Arbitrum resource kinds (Computation, StorageAccess, HistoryGrowth, etc.), but eth_call never produces receipts — the only consumer of this data (state_processor.goreceipt.MultiGasUsed).

This was identified via perf profiling under 100 RPS eth_call load on Arbitrum mainnet:

% CPU Function Category
5.9% addConstantMultiGas Per-opcode constant multi-gas
2.5% MultiGas.SaturatingAddInto Per-opcode dynamic multi-gas
8.4% Total Write-only bookkeeping, discarded for eth_call

Describe the solution you'd like

Add a SkipMultiGas bool field to vm.Config and gate the two hot-path multi-gas accumulation calls in the interpreter loop:

// core/vm/interpreter.go — in the Config struct
SkipMultiGas bool // Skip multi-dimensional gas accounting (RPC-only optimization)

// In Run(), before the main loop:
skipMultiGas = evm.Config.SkipMultiGas

// In the main loop — gate the two hot lines:
if !skipMultiGas {
    addConstantMultiGas(&contract.UsedMultiGas, cost, op)
}
// ... and for dynamic gas:
if !skipMultiGas {
    contract.UsedMultiGas.SaturatingAddInto(multigasDynamicCost)
}

Then set SkipMultiGas: true in the eth_call RPC path (internal/ethapi/api.godoCall):

return applyMessage(ctx, b, args, state, header, timeout, gp, &blockCtx,
    &vm.Config{NoBaseFee: true, SkipMultiGas: true}, precompiles, runCtx)

Safety:

  • Single-dimensional gas (contract.Gas) is completely untouched — gas limits, OOG errors, and gasUsed in responses work identically
  • UsedMultiGas is write-only during EVM interpretation; no execution path reads it to make branching decisions
  • Block production, consensus, Stylus WASM execution, debug_traceCall, and all other RPCs are unaffected (they don't set SkipMultiGas)
  • eth_estimateGas could also benefit if it uses the same code path

Describe alternatives you've considered

  1. Make addConstantMultiGas a no-op function pointer — introduces indirect call overhead in the hot loop, worse than a branch
  2. Compile-time flag — less flexible, prevents using the same binary for both block production and RPC
  3. Skip dynamicGas MultiGas construction entirely — would require changing all dynamicGas function signatures to return uint64 when in skip mode; larger refactor for diminishing returns

Additional context

Benchmarked on Arbitrum mainnet, EPYC 4584PX @ 4.8 GHz, 500-request mix (small to xlarge payloads, from 10KB to 500KB-1MB requests), 100 RPS, 3-minute runs:

Metric Before After (SkipMultiGas) Change
P50 87ms 78ms -10.3%
P90 131ms 122ms -6.9%
P99 178ms 173ms -2.8%
Errors 0 0

The change is 3 lines of logic + 1 config field. The skipMultiGas local variable ensures the branch predictor handles the constant check with zero overhead after the first iteration.

Tested on Nitro v3.9.7 (75e084e).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions