Skip to content

Commit 0c26e7d

Browse files
committed
Use Array for the VM stack to avoid pointer indirection
1 parent dbd440c commit 0c26e7d

4 files changed

Lines changed: 30 additions & 25 deletions

File tree

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"Bash(gh issue:*)",
1212
"Bash(gh api:*)",
1313
"Bash(gh label:*)",
14-
"Bash(find /home/robin/projects/robin/pipe-lang/src/vm /home/robin/projects/robin/pipe-lang/src/interpreter -type f -name *.zig)"
14+
"Bash(find /home/robin/projects/robin/pipe-lang/src/vm /home/robin/projects/robin/pipe-lang/src/interpreter -type f -name *.zig)",
15+
"Bash(./bench.sh 5)"
1516
]
1617
}
1718
}

bench-results.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
22
|:---|---:|---:|---:|---:|
3-
| `./zig-out/bin/pipe samples/fibonacci.pipe` | 77.3 ± 0.5 | 76.9 | 78.1 | 1.04 ± 0.02 |
4-
| `./zig-out/bin/pipe --interp samples/fibonacci.pipe` | 2087.1 ± 28.3 | 2045.0 | 2123.2 | 28.22 ± 0.57 |
5-
| `python3 samples/fibonacci.py` | 74.0 ± 1.1 | 73.2 | 75.8 | 1.00 |
3+
| `./zig-out/bin/pipe samples/fibonacci.pipe` | 78.8 ± 6.8 | 74.8 | 90.9 | 1.00 |
4+
| `./zig-out/bin/pipe --interp samples/fibonacci.pipe` | 2105.1 ± 13.8 | 2089.6 | 2126.3 | 26.71 ± 2.31 |
5+
| `python3 samples/fibonacci.py` | 80.1 ± 1.1 | 78.8 | 81.2 | 1.02 ± 0.09 |

src/tests/bytecode_vm_test.zig

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,6 @@ test "divide by zero is error" {
271271
try std.testing.expectError(error.DivisionByZero, runChunk(&chunk));
272272
}
273273

274-
275274
test "equal integers" {
276275
// 42 == 42 → true
277276
var chunk = Chunk.init(std.testing.allocator);
@@ -352,7 +351,6 @@ test "less than" {
352351
try std.testing.expect(result.eql(.{ .boolean = true }));
353352
}
354353

355-
356354
test "return with empty stack returns unit" {
357355
var chunk = Chunk.init(std.testing.allocator);
358356
defer chunk.deinit();
@@ -787,4 +785,3 @@ test "loop jumps backward" {
787785
const result = try runChunk(&chunk);
788786
try std.testing.expect(result.eql(.{ .int = 6 })); // 3 + 2 + 1
789787
}
790-

src/vm/vm.zig

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const Program = @import("program.zig").Program;
88

99
pub const VmError = error{
1010
DivisionByZero,
11-
TypeError, // wront types for arithmetic/negate/table
11+
TypeError, // wrong types for arithmetic/negate/table
1212
UndefinedVariable,
1313
ArityMismatch,
1414
NotCallable, // Not a function/method that we can call
@@ -27,8 +27,15 @@ pub const Vm = struct {
2727
// Program containing the Chunk + VM tables
2828
program: *const Program,
2929

30+
// Use Arrays rather than ArrayLists for performance.
31+
// The size is fixed anyway, so we don't need to worry about resizing.
32+
// ArrayList data is contiguous but lives on the heap behind a pointer,
33+
// meaning that each access requires a pointer indirection.
34+
// With an Array, the value is just an arithmetic offset off the struct base pointer.
35+
3036
// Value stack
31-
stack: std.ArrayList(Value),
37+
stack: [MAX_STACK]Value,
38+
stack_top: usize,
3239

3340
// Call frame stack
3441
frames: [MAX_FRAMES]CallFrame,
@@ -45,7 +52,8 @@ pub const Vm = struct {
4552
pub fn init(program: *const Program, ctx: RuntimeContext, allocator: std.mem.Allocator) Vm {
4653
return .{
4754
.program = program,
48-
.stack = .{},
55+
.stack = undefined,
56+
.stack_top = 0,
4957
.frames = undefined,
5058
.frame_count = 0,
5159
.globals = .{},
@@ -55,15 +63,13 @@ pub const Vm = struct {
5563
}
5664

5765
pub fn deinit(self: *Vm) void {
58-
self.stack.deinit(self.allocator);
5966
self.globals.deinit(self.allocator);
6067
}
6168

6269
pub fn run(self: *Vm) !Value {
6370
// Setup initial frame for top-level
6471
self.frames[0] = .{ .chunk = &self.program.chunk, .ip = 0, .base_slot = 0 };
6572
self.frame_count = 1;
66-
try self.stack.ensureTotalCapacity(self.allocator, MAX_STACK);
6773
return self.execute();
6874
}
6975

@@ -152,18 +158,18 @@ pub const Vm = struct {
152158
const name = frame.chunk.constants.items[idx].string;
153159

154160
if (self.globals.getPtr(name)) |ptr| {
155-
ptr.* = self.stack.getLast();
161+
ptr.* = self.stack[self.stack_top - 1];
156162
} else {
157163
return error.UndefinedVariable;
158164
}
159165
},
160166
.get_local => {
161167
const idx = frame.base_slot + readU16(frame.chunk, &frame.ip);
162-
self.push(self.stack.items[idx]);
168+
self.push(self.stack[idx]);
163169
},
164170
.set_local => {
165171
const idx = frame.base_slot + readU16(frame.chunk, &frame.ip);
166-
self.stack.items[idx] = self.stack.getLast();
172+
self.stack[idx] = self.stack[self.stack_top - 1];
167173
},
168174
.jump => frame.ip = readU16(frame.chunk, &frame.ip),
169175
.jump_if_false => {
@@ -179,13 +185,13 @@ pub const Vm = struct {
179185
.loop => frame.ip = readU16(frame.chunk, &frame.ip),
180186
.@"return" => {
181187
// Pop the return value (or unit if stack is at base)
182-
const ret_value = if (self.stack.items.len > frame.base_slot)
188+
const ret_value = if (self.stack_top > frame.base_slot)
183189
self.pop()
184190
else
185191
Value.unit;
186192

187193
// Remove all locals of the current stack frame
188-
self.stack.shrinkRetainingCapacity(frame.base_slot);
194+
self.stack_top = frame.base_slot;
189195
self.frame_count -= 1;
190196

191197
// Top-level frame, return the value
@@ -203,8 +209,8 @@ pub const Vm = struct {
203209
const arity = readU8(frame.chunk, &frame.ip);
204210

205211
// Peak the callee to check if we can cast it to *FnObject
206-
const base_slot = self.stack.items.len - 1 - arity;
207-
const callee = self.stack.items[base_slot];
212+
const base_slot = self.stack_top - 1 - arity;
213+
const callee = self.stack[base_slot];
208214

209215
switch (callee) {
210216
.function => {
@@ -225,11 +231,11 @@ pub const Vm = struct {
225231
},
226232
.native => {
227233
// Slice of the fn args
228-
const args = self.stack.items[base_slot + 1 .. base_slot + 1 + arity];
234+
const args = self.stack[base_slot + 1 .. base_slot + 1 + arity];
229235

230236
// Call builtin function, remove the args and push the return value
231237
const ret_value = callee.native.func(args, self.ctx);
232-
self.stack.shrinkRetainingCapacity(base_slot);
238+
self.stack_top = base_slot;
233239
self.push(ret_value);
234240
},
235241
else => return error.NotCallable,
@@ -245,14 +251,15 @@ pub const Vm = struct {
245251

246252
// Pushes the value onto the stack
247253
fn push(self: *Vm, value: Value) void {
248-
self.stack.appendAssumeCapacity(value);
254+
self.stack[self.stack_top] = value;
255+
self.stack_top += 1;
249256
}
250257

251258
// Pops the value from the stack
252259
fn pop(self: *Vm) Value {
253-
std.debug.assert(self.stack.items.len > 0);
254-
// Unwrap the nullable or crash. Life is hard.
255-
return self.stack.pop().?;
260+
std.debug.assert(self.stack_top > 0);
261+
self.stack_top -= 1;
262+
return self.stack[self.stack_top];
256263
}
257264

258265
// Read a 8-bit value from the code

0 commit comments

Comments
 (0)