mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-01-09 20:06:52 +00:00
8857fccb44
In the current VM opcode dispatcher, a two-level case statement is generated that first matches the opcode and then uses another nested case statement to select the actual implementation based on which fork it is, causing the dispatcher to grow by `O(opcodes) * O(forks)`. The fork does not change between instructions causing significant inefficiency for this approach - not only because it repeats the fork lookup but also because of code size bloat and missed optimizations. A second source of inefficiency in dispatching is the tracer code which in the vast majority of cases is disabled but nevertheless sees multiple conditionals being evaluated for each instruction only to remain disabled throughout exeuction. This PR rewrites the opcode dispatcher macro to generate a separate dispatcher for each fork and tracer setting and goes on to pick the right one at the start of the computation. This has many advantages: * much smaller dispatcher * easier to compile * better inlining * fewer pointlessly repeated instruction * simplified macro (!) * slow "low-compiler-memory" dispatcher code can be removed Net block import improvement at about 4-6% depending on the contract - synthetic EVM benchmnarks would show an even better result most likely.
137 lines
4.4 KiB
Nim
137 lines
4.4 KiB
Nim
# Nimbus
|
|
# Copyright (c) 2018-2024 Status Research & Development GmbH
|
|
# Licensed under either of
|
|
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
|
# http://www.apache.org/licenses/LICENSE-2.0)
|
|
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
|
# http://opensource.org/licenses/MIT)
|
|
# at your option. This file may not be copied, modified, or distributed except
|
|
# according to those terms.
|
|
|
|
const
|
|
# debugging flag, dump macro info when asked for
|
|
noisy {.intdefine.}: int = 0
|
|
# isNoisy {.used.} = noisy > 0
|
|
isChatty {.used.} = noisy > 1
|
|
|
|
import
|
|
../computation,
|
|
../evm_errors,
|
|
../../common/evmforks,
|
|
./gas_costs,
|
|
./gas_meter,
|
|
./op_codes,
|
|
./op_handlers,
|
|
./op_handlers/oph_defs,
|
|
macros
|
|
|
|
export EVMFork, Op, oph_defs, gas_meter
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Helpers
|
|
# ------------------------------------------------------------------------------
|
|
|
|
template handleStopDirective(cpt: VmCpt, tracingEnabled: bool) =
|
|
#trace "op: Stop"
|
|
when tracingEnabled:
|
|
if not cpt.code.atEnd():
|
|
# we only trace `REAL STOP` and ignore `FAKE STOP`
|
|
cpt.opIndex = cpt.traceOpCodeStarted(Stop)
|
|
cpt.traceOpCodeEnded(Stop, cpt.opIndex)
|
|
|
|
template handleFixedGasCostsDirective(
|
|
fork: EVMFork, op: Op, cost: GasInt, cpt: VmCpt, tracingEnabled: bool
|
|
) =
|
|
when tracingEnabled:
|
|
cpt.opIndex = cpt.traceOpCodeStarted(op)
|
|
|
|
?cpt.opcodeGasCost(op, cost, tracingEnabled, reason = $op)
|
|
?vmOpHandlers[fork][op].run(cpt)
|
|
|
|
# If continuation is not nil, traceOpCodeEnded will be called in executeOpcodes.
|
|
when tracingEnabled:
|
|
if cpt.continuation.isNil:
|
|
cpt.traceOpCodeEnded(op, cpt.opIndex)
|
|
|
|
template handleOtherDirective(fork: EVMFork, op: Op, cpt: VmCpt, tracingEnabled: bool) =
|
|
when tracingEnabled:
|
|
cpt.opIndex = cpt.traceOpCodeStarted(op)
|
|
|
|
?vmOpHandlers[fork][op].run(cpt)
|
|
|
|
# If continuation is not nil, traceOpCodeEnded will be called in executeOpcodes.
|
|
when tracingEnabled:
|
|
if cpt.continuation.isNil:
|
|
cpt.traceOpCodeEnded(op, cpt.opIndex)
|
|
|
|
proc makeCaseDispatcher(forkArg: EVMFork, tracingEnabled: bool, opArg, cpt: NimNode): NimNode =
|
|
# Create a case statement for dispatching opcode to handler for the given
|
|
# fork, taking care to record the gas cost
|
|
# TODO there are several forks for which neither opcodes nor gas costs
|
|
# changed - these could use the same dispatcher thus saving some space
|
|
# and compile time
|
|
let gasCosts = forkToSchedule(forkArg)
|
|
|
|
result = nnkCaseStmt.newTree(opArg)
|
|
for op in Op:
|
|
let
|
|
asOp = quote: `op`
|
|
handler =
|
|
if op == Stop:
|
|
quote:
|
|
handleStopDirective(`cpt`, `tracingEnabled`)
|
|
elif gasCosts[op].kind == GckFixed:
|
|
let cost = gasCosts[op].cost
|
|
quote:
|
|
handleFixedGasCostsDirective(
|
|
`forkArg`, `op`, `cost`, `cpt`, `tracingEnabled`
|
|
)
|
|
else:
|
|
quote:
|
|
handleOtherDirective(`forkArg`, `op`, `cpt`, `tracingEnabled`)
|
|
branch =
|
|
case op
|
|
of Create, Create2, Call, CallCode, DelegateCall, StaticCall:
|
|
# These opcodes use `chainTo` to create a continuation call which must
|
|
# be handled separately
|
|
quote:
|
|
`handler`
|
|
if not `cpt`.continuation.isNil:
|
|
break
|
|
|
|
of Stop, Return, Revert, SelfDestruct:
|
|
quote:
|
|
`handler`
|
|
break
|
|
else:
|
|
handler
|
|
|
|
result.add nnkOfBranch.newTree(asOp, branch)
|
|
|
|
when isChatty:
|
|
echo ">>> ", result.repr
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public macros/functions
|
|
# ------------------------------------------------------------------------------
|
|
|
|
macro dispatchInstr*(
|
|
fork: static EVMFork, tracingEnabled: static bool, op: Op, cpt: VmCpt
|
|
): untyped =
|
|
makeCaseDispatcher(fork, tracingEnabled, op, cpt)
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Debugging ...
|
|
# ------------------------------------------------------------------------------
|
|
|
|
when isMainModule and isChatty:
|
|
import ../types
|
|
|
|
proc optimised(cpt: VmCpt): EvmResultVoid {.compileTime.} =
|
|
while true:
|
|
dispatchInstr(FkFrontier, false, cpt.instr, cpt)
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# End
|
|
# ------------------------------------------------------------------------------
|