Part 7 · 1 chapters · ~8 min

Bytecode VMs

Stack versus register VMs, the fetch-decode-execute loop of the BYO VM, frames and calls, dispatch techniques (switch, threaded code, computed goto), superinstructions, inline caches, measured interpreter versus JIT cost, tiered compilation in V8 and HotSpot, and CPython's specialising interpreter.

8

A VM in 55 lines

code
// from src/vm.ts: an operand stack, a call stack of frames, and a loop
for (;;) {
  if (++steps > max) throw new Error(`step limit exceeded (${max}): infinite loop?`);
  const op = c.code[pc++];
  const frame = frames[frames.length - 1];
  switch (op[0]) {
    case 'PUSH': stack.push(op[1]); break;
    case 'LOAD': stack.push(frame.locals[op[1]]); break;
    case 'ADD': { const b = stack.pop()!, a = stack.pop()!; stack.push((a + b) | 0); break; }   // 32-bit ints
    case 'JZ': if (stack.pop() === 0) pc = op[1]; break;
    case 'CALL': { const f = c.fns.get(op[1])!; const locals = new Array(f.locals).fill(0);
      for (let k = op[2] - 1; k >= 0; k--) locals[k] = stack.pop()!;
      frames.push({ ret: pc, locals }); pc = f.entry; break; }
    case 'RET': { const f = frames.pop()!; pc = f.ret; break; }   // the return value stays on the stack
    case 'HALT': return { output, steps };
  }
}

Register VMs (Lua 5, Dalvik) name operands in each instruction (ADD r1, r2, r3), executing fewer instructions than stack VMs. CPython 3.11+ specialises instructions at run time (LOAD_ATTR becomes LOAD_ATTR_INSTANCE_VALUE after seeing the same type repeatedly), part of the "Faster CPython" work that made 3.11 about 25% faster on average by the project's benchmarks.

INTERPRETER VERSUS JIT, MEASURED
fib(25) = 75,025, Node 25.7 on an Apple M3 Pro
BYO stack VM, wall time incl. Node startup~150 msNode startup alone (node -e 0)~73 mssame function in JavaScript, V8 JIT0.9 ms
swipe the figure sideways, or tap expand for full screen
1/4
the interpreter
The BYO VM is a switch inside a loop over arrays of instructions, written in JavaScript: about 150 ms of wall time for fib(25), of which roughly 70 ms is starting Node.
~80 ms of interpretationa switch in a loop