0154 - The VM dispatch loop uses no goto
Date: 2026-10-16
Area: bytecode VM, build
Related: ADR 0003, ADR 0023
Context
TGocciaVM.ExecuteClosureRegistersInternal runs every bytecode instruction.
Since #1216 it had two
loop heads, a production head without the coverage, opcode-profiling, stop-IP
and instruction-limit checks and an instrumented head with them, and jumped
between them, the shared opcode case in Goccia.VM.DispatchCase.inc and the
loop end with goto. The case itself left early with 23 goto LDispatchNext
and entered OP_GET_PROP_CONST from OP_GET_LOCAL_PROP_CONST through a label.
FPC's wasm32 code generator has no arbitrary jumps. It lowers a function that
uses goto into a state machine over the whole function body: every label
becomes a state, and the function re-dispatches on the state at each jump.
With a patched trunk compiler that dispatches the states through one
br_table, the goto version of Richards still executed 3.07 times the
instructions of a structured one under wasmtime. A wasm32 build therefore
needed a second, structured loop behind a define, which the wasm32 port
prototype carried as GOCCIA_VM_STRUCTURED_DISPATCH, and the targets would
have run different dispatch code.
Decision
The dispatch loop is one while loop with no labels. Its body fetches and
decodes the next instruction through the production or the instrumented path,
chosen once per entry into the try block as before, then runs the opcode
case and the memory-pressure check. In the case, Continue replaces
goto LDispatchNext and skips the memory-pressure check, as the jump did.
OP_GET_LOCAL_PROP_CONST shares OP_GET_PROP_CONST's case arm and loads its
local before the shared property read. Every target compiles this one loop;
there is no define for another.
Consequences
- Native cost does not rise. With FPC 3.2.2 and the production build, callgrind
counts 0.1% to 0.9% fewer instructions for each of 13 AWFY workloads and 4
micro workloads than the
gotoversion at 02e2dc1a did (AWFY geomean 0.997x). AnOP_GET_PROP_CONSTnow tests the opcode once before its property read. - A wasm32 build compiles the same dispatch loop as a native one.
ContinueandBreakinGoccia.VM.DispatchCase.incact on the dispatch loop only outside an inner loop; inside afororwhilein a case arm they act on that inner loop. ABreakout of the dispatch loop does not end execution: the enclosing loop around the exception handlers enters it again at the same instruction. An opcode ends execution withExitor by raising.gotostays out of the VM (see the contributor notes in Bytecode VM).