re-implemented handler-call statement by doubly nested case statement

why:
  the new implementation lost more then 25% execution time on the test
  suite when compared to the original VM. so the handler call and the
  surrounding statements have been wrapped in a big case statement similar
  to the original VM implementation. on Linux/x64, the execution time of
  the new VM2 seems to be on par with the old VM.

details:
  on Linux/x64, computed goto works and is activated with the -d:release
  flag. here the execution time of the new VM2 was tested short of 0.02%
  better than the old VM. without the computed goto, it is short of
  0.4% slower than the old VM.
This commit is contained in:
Jordan Hrycaj 2021-04-22 12:02:13 +01:00 committed by zah
parent 3ed234e0a1
commit 1b3117edbd
4 changed files with 196 additions and 49 deletions

View File

@ -14,6 +14,14 @@
## See `here <../../ex/vm/interpreter/forks_list.html>`_ for an ## See `here <../../ex/vm/interpreter/forks_list.html>`_ for an
## overview. ## overview.
## ##
## Assumptions on the naming of the fork list:
## * each symbol start with the prefix "Fk"
## * the the first word of the prettified text representaion is the same
## text as the one following the "Fk" symbol name (irrespective of
## character case.)
import
strutils
type type
Fork* = enum Fork* = enum
@ -27,4 +35,9 @@ type
FkIstanbul = "istanbul" FkIstanbul = "istanbul"
FkBerlin = "berlin" FkBerlin = "berlin"
proc toSymbolName*(fork: Fork): string =
## Given a `fork` argument, print the symbol name so that it can be used
## im macro staements.
"Fk" & ($fork).split(' ')[0]
# End # End

View File

@ -208,6 +208,12 @@ type
SelfDestruct = 0xff ## Halt execution and register account for later SelfDestruct = 0xff ## Halt execution and register account for later
## deletion. ## deletion.
proc toSymbolName*(op: Op): string =
## Given an `op` argument, print the symbol name so that it can be used
## im macro staements.
$op
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Verify that Op is contiguous and sym names follow some standards # Verify that Op is contiguous and sym names follow some standards
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

@ -108,10 +108,10 @@ proc mkOpTable(selected: Fork): array[Op,Vm2OpExec] {.compileTime.} =
# rc # rc
type type
hdlRec = tuple vmOpHandlersRec* = tuple
name: string name: string ## Name (or ID) of op handler
info: string info: string ## Some op handler info
run: Vm2OpFn run: Vm2OpFn ## Executable handler
const const
# Pack handler record. # Pack handler record.
@ -131,33 +131,35 @@ const
# under what circumstances the vm2OpHandlers[] matrix is set up correctly. # under what circumstances the vm2OpHandlers[] matrix is set up correctly.
# Linearising/flattening the index has no effect here. # Linearising/flattening the index has no effect here.
# #
vmOpHandlers = block: vmOpHandlers* = ## Op handler records matrix indexed `fork` x `op`
var rc: array[Fork, array[Op, hdlRec]] block:
for fork in Fork: var rc: array[Fork, array[Op, vmOpHandlersRec]]
var tab = fork.mkOpTable for fork in Fork:
for op in Op: var tab = fork.mkOpTable
rc[fork][op].name = tab[op].name for op in Op:
rc[fork][op].info = tab[op].info rc[fork][op].name = tab[op].name
rc[fork][op].run = tab[op].exec.run rc[fork][op].info = tab[op].info
rc rc[fork][op].run = tab[op].exec.run
rc
proc opHandlersRun*(fork: Fork; op: Op; d: var Vm2Ctx) {.inline.} =
## Given a particular `fork` and an `op`-code, run the associated handler
vmOpHandlers[fork][op].run(d)
proc opHandlersName*(fork: Fork; op: Op): string =
## Get name (or ID) of op handler
vmOpHandlers[fork][op].name
proc opHandlersInfo*(fork: Fork; op: Op): string =
## Get some op handler info
vmOpHandlers[fork][op].info
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Debugging ... # Debugging ...
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
when isMainModule and isNoisy: when isMainModule and isNoisy:
proc opHandlersRun(fork: Fork; op: Op; d: var Vm2Ctx) {.used.} =
## Given a particular `fork` and an `op`-code, run the associated handler
vmOpHandlers[fork][op].run(d)
proc opHandlersName(fork: Fork; op: Op): string {.used.} =
## Get name (or ID) of op handler
vmOpHandlers[fork][op].name
proc opHandlersInfo(fork: Fork; op: Op): string {.used.} =
## Get some op handler info
vmOpHandlers[fork][op].info
echo ">>> berlin[shl]: ", FkBerlin.opHandlersInfo(Shl) echo ">>> berlin[shl]: ", FkBerlin.opHandlersInfo(Shl)
echo ">>> berlin[push32]: ", FkBerlin.opHandlersInfo(Push32) echo ">>> berlin[push32]: ", FkBerlin.opHandlersInfo(Push32)
echo ">>> berlin[dup16]: ", FkBerlin.opHandlersInfo(Dup16) echo ">>> berlin[dup16]: ", FkBerlin.opHandlersInfo(Dup16)

View File

@ -8,51 +8,177 @@
# at your option. This file may not be copied, modified, or distributed except # at your option. This file may not be copied, modified, or distributed except
# according to those terms. # according to those terms.
const
# debugging flag
kludge {.intdefine.}: int = 0
breakCircularDependency {.used.} = kludge > 0
import import
./compu_helper, ./compu_helper,
./interpreter/[forks_list, gas_costs, gas_meter, ./interpreter/[forks_list, gas_costs, gas_meter,
op_codes, op_handlers, op_handlers/oph_defs], op_codes, op_handlers, op_handlers/oph_defs],
./code_stream, ./code_stream,
./v2types, ./v2types,
chronicles chronicles,
macros
logScope: logScope:
topics = "vm opcode" topics = "vm opcode"
# ------------------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------------------
template handleStopDirective(k: var Vm2Ctx) =
trace "op: Stop"
if not k.cpt.code.atEnd() and k.cpt.tracingEnabled:
# we only trace `REAL STOP` and ignore `FAKE STOP`
k.cpt.opIndex = k.cpt.traceOpCodeStarted(Stop)
k.cpt.traceOpCodeEnded(Stop, k.cpt.opIndex)
template handleFixedGasCostsDirective(fork: Fork; op: Op; k: var Vm2Ctx) =
if k.cpt.tracingEnabled:
k.cpt.opIndex = k.cpt.traceOpCodeStarted(op)
k.cpt.gasMeter.consumeGas(k.cpt.gasCosts[op].cost, reason = $op)
vmOpHandlers[fork][op].run(k)
if k.cpt.tracingEnabled:
k.cpt.traceOpCodeEnded(op, k.cpt.opIndex)
template handleOtherDirective(fork: Fork; op: Op; k: var Vm2Ctx) =
if k.cpt.tracingEnabled:
k.cpt.opIndex = k.cpt.traceOpCodeStarted(op)
vmOpHandlers[fork][op].run(k)
if k.cpt.tracingEnabled:
k.cpt.traceOpCodeEnded(op, k.cpt.opIndex)
# ------------------------------------------------------------------------------
# Private, big nasty doubly nested case matrix generator
# ------------------------------------------------------------------------------
# reminiscent of Mamy's opTableToCaseStmt() from original VM
proc toCaseStmt(forkArg, opArg, k: NimNode): NimNode =
# Outer case/switch => Op
let branchOnOp = quote do: `opArg`
result = nnkCaseStmt.newTree(branchOnOp)
for op in Op:
# Inner case/switch => Fork
let branchOnFork = quote do: `forkArg`
var forkCaseSubExpr = nnkCaseStmt.newTree(branchOnFork)
for fork in Fork:
let
asFork = quote do: Fork(`fork`)
asOp = quote do: Op(`op`)
let branchStmt = block:
if op == Stop:
quote do:
handleStopDirective(`k`)
elif BaseGasCosts[op].kind == GckFixed:
quote do:
handleFixedGasCostsDirective(`asFork`,`asOp`,`k`)
else:
quote do:
handleOtherDirective(`asFork`,`asOp`,`k`)
forkCaseSubExpr.add nnkOfBranch.newTree(
newIdentNode(fork.toSymbolName),
branchStmt)
# Wrap innner case/switch into outer case/switch
let branchStmt = block:
case op
of Create, Create2, Call, CallCode, DelegateCall, StaticCall:
quote do:
`forkCaseSubExpr`
if not c.continuation.isNil:
break
of Stop, Return, Revert, SelfDestruct:
quote do:
`forkCaseSubExpr`
break
else:
quote do:
`forkCaseSubExpr`
result.add nnkOfBranch.newTree(
newIdentNode(op.toSymbolName),
branchStmt)
when breakCircularDependency:
echo ">>> ", result.repr
macro genDispatchMatrix(fork: Fork; op: Op; k: Vm2Ctx): untyped =
result = fork.toCaseStmt(op, k)
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
proc selectVM*(c: Computation, fork: Fork) {.gcsafe.} = proc selectVM*(c: Computation, fork: Fork) {.gcsafe.} =
## Op code execution handler main loop.
var desc: Vm2Ctx var desc: Vm2Ctx
desc.cpt = c
if c.tracingEnabled: if c.tracingEnabled:
c.prepareTracer() c.prepareTracer()
while true: while true:
c.instr = c.code.next() c.instr = c.code.next()
var op = c.instr
if op == Stop: # Note Mamy's observation in opTableToCaseStmt() from original VM
trace "op: Stop" # regarding computed goto
if not c.code.atEnd() and c.tracingEnabled: #
c.opIndex = c.traceOpCodeStarted(Stop) # ackn:
c.traceOpCodeEnded(Stop, c.opIndex) # #{.computedGoto.}
break # # computed goto causing stack overflow, it consumes a lot of space
# # we could use manual jump table instead
# # TODO lots of macro magic here to unravel, with chronicles...
# # `c`.logger.log($`c`.stack & "\n\n", fgGreen)
if c.tracingEnabled: when defined(release):
c.opIndex = c.traceOpCodeStarted(op) #
if BaseGasCosts[op].kind == GckFixed: # FIXME: OS case list below needs to be adjusted
c.gasMeter.consumeGas(c.gasCosts[op].cost, reason = $op) #
when defined(windows):
when defined(cpu64):
{.warning: "*** Win64/VM2 handler switch => computedGoto".}
{.computedGoto, optimization: speed.}
else:
# computedGoto is not compiling on github/ci (out of memory) -- jordan
{.warning: "*** Win32/VM2 handler switch => optimisation disabled".}
# {.computedGoto, optimization: speed.}
desc.cpt = c elif defined(linux):
opHandlersRun(fork, op, desc) when defined(cpu64):
{.warning: "*** Linux64/VM2 handler switch => computedGoto".}
{.computedGoto, optimization: speed.}
else:
{.warning: "*** Linux32/VM2 handler switch => computedGoto".}
{.computedGoto, optimization: speed.}
if c.tracingEnabled: elif defined(macosx):
c.traceOpCodeEnded(op, c.opIndex) when defined(cpu64):
{.warning: "*** MacOs64/VM2 handler switch => computedGoto".}
{.computedGoto, optimization: speed.}
else:
{.warning: "*** MacOs32/VM2 handler switch => computedGoto".}
{.computedGoto, optimization: speed.}
case op else:
of Create, Create2, Call, CallCode, DelegateCall, StaticCall: {.warning: "*** Unsupported OS => no handler switch optimisation".}
if not c.continuation.isNil:
break
of Return, Revert, SelfDestruct:
break
else:
discard
genDispatchMatrix(fork, c.instr, desc)
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------