Exercise 1: Scaling the Benchmark to 601 Nodes — Possible Solution ==================================================================== THE SETUP ------------------------------ def build_left_deep(depth): ops = ['+', '-', '*'] node = Literal(1.0) for i in range(depth): node = Binary(node, ops[i % 3], Literal(2.0)) return node tree = build_left_deep(300) # 601 nodes total bytecode = [] compile_expr(tree, bytecode) N = 20000 # time tree.accept(TreeWalkEvaluator()) x N, and run_vm(bytecode) x N, # taking the median of several repeats to cancel out system noise RESULTS ------------------------------ tree-walk (median of 9 runs): 84.893 microseconds/eval bytecode VM (median of 9 runs): 49.620 microseconds/eval ratio: 1.711x -- the VM is faster, by roughly the same margin measured at 201 nodes in the chapter itself (1.678x) WHY THIS WORKS AS AN ANSWER ------------------------------ The ratio does NOT keep growing without bound between 201 and 601 nodes -- it holds essentially flat, just above 1.7x, at both sizes. This is the expected shape once you understand WHY the VM pulls ahead in the first place: the tree-walker's cost is dominated by a fixed per-node overhead (two Python function calls per node, for accept() then visit_binary()) that scales linearly with node count, same as the VM's own per-instruction cost scales linearly with instruction count. Once the program is large enough that this per-node/per-instruction cost dominates over any fixed startup cost, the RATIO between the two approaches settles into a stable constant determined by how much heavier a Python function call is than a single loop iteration -- it doesn't keep climbing forever just because the program gets bigger. The chapter's own honest finding holds up under a further 3x size increase: past a certain program size, bytecode wins by a consistent, predictable margin, not an ever-growing one.