Jacek Sieka 8857fccb44
create per-fork opcode dispatcher (#2579)
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.
2024-08-28 10:20:36 +02:00

47 lines
1.8 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.
{.push raises: [].}
import
eth/common, # GasInt
../evm_errors,
../types
func init*(m: var GasMeter, startGas: GasInt) =
m.gasRemaining = startGas
m.gasRefunded = 0
func consumeGas*(
gasMeter: var GasMeter; amount: GasInt; reason: static string): EvmResultVoid {.inline.} =
# consumeGas is a hotspot in the vm due to it being called for every
# instruction
# TODO report reason - consumeGas is a hotspot in EVM execution so it has to
# be done carefully
if amount > gasMeter.gasRemaining:
return err(gasErr(OutOfGas))
gasMeter.gasRemaining -= amount
ok()
func returnGas*(gasMeter: var GasMeter; amount: GasInt) =
gasMeter.gasRemaining += amount
func refundGas*(gasMeter: var GasMeter; amount: int64) =
# EIP-2183 Net gas metering for sstore is built upon idea
# that the refund counter is only one in an EVM like geth does.
# EIP-2183 gurantee that the counter can never go below zero.
# But nimbus, EVMC, and emvone taken different route, the refund counter
# is present at each level of recursion. That's why EVMC/evmone is using
# int64 while geth using uint64 for their gas calculation.
# After nimbus converting GasInt to uint64, the gas refund
# cannot be converted to uint64 too, because the sum of all children gas refund,
# no matter positive or negative will be >= 0 when EVM finish execution.
gasMeter.gasRefunded += amount