From 1b3117edbdf37fe99beb3c222910b2fc059368f7 Mon Sep 17 00:00:00 2001 From: Jordan Hrycaj Date: Thu, 22 Apr 2021 12:02:13 +0100 Subject: [PATCH] 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. --- nimbus/vm2/interpreter/forks_list.nim | 13 ++ nimbus/vm2/interpreter/op_codes.nim | 6 + nimbus/vm2/interpreter/op_handlers.nim | 52 ++++---- nimbus/vm2/interpreter_dispatch.nim | 174 +++++++++++++++++++++---- 4 files changed, 196 insertions(+), 49 deletions(-) diff --git a/nimbus/vm2/interpreter/forks_list.nim b/nimbus/vm2/interpreter/forks_list.nim index 2d2f92fae..9d11e1a7f 100644 --- a/nimbus/vm2/interpreter/forks_list.nim +++ b/nimbus/vm2/interpreter/forks_list.nim @@ -14,6 +14,14 @@ ## See `here <../../ex/vm/interpreter/forks_list.html>`_ for an ## 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 Fork* = enum @@ -27,4 +35,9 @@ type FkIstanbul = "istanbul" 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 diff --git a/nimbus/vm2/interpreter/op_codes.nim b/nimbus/vm2/interpreter/op_codes.nim index 819122f1d..9b96c6a28 100644 --- a/nimbus/vm2/interpreter/op_codes.nim +++ b/nimbus/vm2/interpreter/op_codes.nim @@ -208,6 +208,12 @@ type SelfDestruct = 0xff ## Halt execution and register account for later ## 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 # ------------------------------------------------------------------------------ diff --git a/nimbus/vm2/interpreter/op_handlers.nim b/nimbus/vm2/interpreter/op_handlers.nim index badad1342..36c0a3f76 100644 --- a/nimbus/vm2/interpreter/op_handlers.nim +++ b/nimbus/vm2/interpreter/op_handlers.nim @@ -108,10 +108,10 @@ proc mkOpTable(selected: Fork): array[Op,Vm2OpExec] {.compileTime.} = # rc type - hdlRec = tuple - name: string - info: string - run: Vm2OpFn + vmOpHandlersRec* = tuple + name: string ## Name (or ID) of op handler + info: string ## Some op handler info + run: Vm2OpFn ## Executable handler const # Pack handler record. @@ -131,33 +131,35 @@ const # under what circumstances the vm2OpHandlers[] matrix is set up correctly. # Linearising/flattening the index has no effect here. # - vmOpHandlers = block: - var rc: array[Fork, array[Op, hdlRec]] - for fork in Fork: - var tab = fork.mkOpTable - for op in Op: - rc[fork][op].name = tab[op].name - rc[fork][op].info = tab[op].info - 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 + vmOpHandlers* = ## Op handler records matrix indexed `fork` x `op` + block: + var rc: array[Fork, array[Op, vmOpHandlersRec]] + for fork in Fork: + var tab = fork.mkOpTable + for op in Op: + rc[fork][op].name = tab[op].name + rc[fork][op].info = tab[op].info + rc[fork][op].run = tab[op].exec.run + rc # ------------------------------------------------------------------------------ # Debugging ... # ------------------------------------------------------------------------------ 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[push32]: ", FkBerlin.opHandlersInfo(Push32) echo ">>> berlin[dup16]: ", FkBerlin.opHandlersInfo(Dup16) diff --git a/nimbus/vm2/interpreter_dispatch.nim b/nimbus/vm2/interpreter_dispatch.nim index db861978c..2c5e050bf 100644 --- a/nimbus/vm2/interpreter_dispatch.nim +++ b/nimbus/vm2/interpreter_dispatch.nim @@ -8,51 +8,177 @@ # at your option. This file may not be copied, modified, or distributed except # according to those terms. +const + # debugging flag + kludge {.intdefine.}: int = 0 + breakCircularDependency {.used.} = kludge > 0 + import ./compu_helper, ./interpreter/[forks_list, gas_costs, gas_meter, op_codes, op_handlers, op_handlers/oph_defs], ./code_stream, ./v2types, - chronicles + chronicles, + macros logScope: 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.} = + ## Op code execution handler main loop. var desc: Vm2Ctx + desc.cpt = c if c.tracingEnabled: c.prepareTracer() while true: c.instr = c.code.next() - var op = c.instr - if op == Stop: - trace "op: Stop" - if not c.code.atEnd() and c.tracingEnabled: - c.opIndex = c.traceOpCodeStarted(Stop) - c.traceOpCodeEnded(Stop, c.opIndex) - break + # Note Mamy's observation in opTableToCaseStmt() from original VM + # regarding computed goto + # + # ackn: + # #{.computedGoto.} + # # 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: - c.opIndex = c.traceOpCodeStarted(op) - if BaseGasCosts[op].kind == GckFixed: - c.gasMeter.consumeGas(c.gasCosts[op].cost, reason = $op) + when defined(release): + # + # FIXME: OS case list below needs to be adjusted + # + 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 - opHandlersRun(fork, op, desc) + elif defined(linux): + 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: - c.traceOpCodeEnded(op, c.opIndex) + elif defined(macosx): + when defined(cpu64): + {.warning: "*** MacOs64/VM2 handler switch => computedGoto".} + {.computedGoto, optimization: speed.} + else: + {.warning: "*** MacOs32/VM2 handler switch => computedGoto".} + {.computedGoto, optimization: speed.} - case op - of Create, Create2, Call, CallCode, DelegateCall, StaticCall: - if not c.continuation.isNil: - break - of Return, Revert, SelfDestruct: - break - else: - discard + else: + {.warning: "*** Unsupported OS => no handler switch optimisation".} + genDispatchMatrix(fork, c.instr, desc) + +# ------------------------------------------------------------------------------ +# End +# ------------------------------------------------------------------------------