Exercise 3: Integer Opcodes Instead of String Comparisons — Possible Solution ==================================================================== THE FIX ------------------------------ OP_PUSH, OP_ADD, OP_SUB, OP_MUL, OP_DIV = range(5) def compile_expr2(node, out): if isinstance(node, Literal): out.append((OP_PUSH, node.value)) elif isinstance(node, Binary): compile_expr2(node.left, out) compile_expr2(node.right, out) opcode = {'+': OP_ADD, '-': OP_SUB, '*': OP_MUL, '/': OP_DIV}[node.op] out.append((opcode, 0.0)) def run_vm2(code): stack = [] push, pop = stack.append, stack.pop for op, val in code: if op == OP_PUSH: push(val) elif op == OP_ADD: b = pop(); a = pop(); push(a + b) elif op == OP_SUB: b = pop(); a = pop(); push(a - b) elif op == OP_MUL: b = pop(); a = pop(); push(a * b) else: b = pop(); a = pop(); push(a / b) return pop() RESULTS ------------------------------ Original (string-opcode) VM vs. tree-walk, 17 nodes: ratio ~0.77-0.85x (VM slower) Int-opcode VM vs. tree-walk, same 17-node expression: ratio ~0.90x (VM still slower, gap narrower) WHY THIS WORKS AS AN ANSWER ------------------------------ Switching from string comparisons ('PUSH', 'ADD', ...) to small integers narrows the gap but does NOT close it -- the VM is still slower than tree-walking at this small scale, just less slower. This is an honest, useful negative result: it confirms the chapter's own explanation was correct. The bottleneck was never really "string comparison is slow in Python" (integer equality checks are cheap too, and the improvement from switching is real but modest). The bottleneck is structural: this VM is a Python for-loop, interpreting a second, separate instruction format, still running entirely inside Python's own interpreter -- there's no way to make that loop as cheap as a single native dispatch step the way CPython's own C-level bytecode interpreter (or a hand-written VM in C, which Chapters 2+ build toward conceptually even though this course's own Wisp VM stays in Python) can achieve. Micro-optimizing the dispatch mechanism inside Python helps at the margins, but it can't erase the fact that a small, 17-instruction program simply doesn't run long enough to amortize the cost of a second interpretation layer on top of Python's own. This is exactly consistent with the chapter's own larger-scale finding: the win from bytecode shows up once programs get big enough that the compounding, per-node cost of RECURSIVE dispatch (not opcode dispatch specifically) starts to dominate -- and no opcode-encoding trick changes that underlying shape.