Skip to content

Commit 22405c4

Browse files
committed
perf(vm): inline operand stack append fast path (#46)
1 parent 2ebda1a commit 22405c4

1 file changed

Lines changed: 57 additions & 2 deletions

File tree

src/vm.zig

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,68 @@ pub const Frame = struct {
9090
}
9191
};
9292

93+
/// The VM operand stack has a much hotter append path than a general-purpose
94+
/// ArrayList: almost every bytecode instruction pushes at least one Value. The
95+
/// standard unmanaged append calls ensureTotalCapacity even when capacity is
96+
/// already available; profiles attributed roughly a third of arithmetic VM
97+
/// samples to that call/check boundary. Keep ArrayList growth semantics on the
98+
/// rare full-capacity path, but make the common append one inlined comparison
99+
/// and one store.
100+
const OperandStack = struct {
101+
items: []Value = &.{},
102+
capacity: usize = 0,
103+
104+
pub const empty: OperandStack = .{};
105+
106+
pub inline fn append(self: *OperandStack, allocator: std.mem.Allocator, item: Value) std.mem.Allocator.Error!void {
107+
if (self.items.len == self.capacity) try self.grow(allocator);
108+
self.items.len += 1;
109+
self.items[self.items.len - 1] = item;
110+
}
111+
112+
noinline fn grow(self: *OperandStack, allocator: std.mem.Allocator) std.mem.Allocator.Error!void {
113+
var list: std.ArrayListUnmanaged(Value) = .{
114+
.items = self.items,
115+
.capacity = self.capacity,
116+
};
117+
try list.ensureTotalCapacity(allocator, self.items.len + 1);
118+
self.items = list.items;
119+
self.capacity = list.capacity;
120+
}
121+
122+
pub inline fn pop(self: *OperandStack) ?Value {
123+
if (self.items.len == 0) return null;
124+
const item = self.items[self.items.len - 1];
125+
self.items.len -= 1;
126+
return item;
127+
}
128+
129+
pub inline fn shrinkRetainingCapacity(self: *OperandStack, new_len: usize) void {
130+
std.debug.assert(new_len <= self.items.len);
131+
self.items.len = new_len;
132+
}
133+
134+
pub inline fn clearRetainingCapacity(self: *OperandStack) void {
135+
self.items.len = 0;
136+
}
137+
138+
pub fn deinit(self: *OperandStack, allocator: std.mem.Allocator) void {
139+
var list: std.ArrayListUnmanaged(Value) = .{
140+
.items = self.items,
141+
.capacity = self.capacity,
142+
};
143+
list.deinit(allocator);
144+
self.* = .empty;
145+
}
146+
};
147+
93148
/// A resumable execution state: the operand stack, completion accumulator, and
94149
/// instruction pointer. For a normal call this lives on the host stack and is
95150
/// thrown away when `run` returns; for a generator it lives in the `Generator`
96151
/// and persists across `yield`/resume, which is what makes suspension faithful
97152
/// (the whole operand stack is saved, so a `yield` can sit mid-expression).
98153
pub const Exec = struct {
99-
stack: std.ArrayListUnmanaged(Value) = .empty,
154+
stack: OperandStack = .empty,
100155
acc: Value = Value.undef(),
101156
ip: usize = 0,
102157
/// The activation frame this Exec is running (null for env-mode bodies:
@@ -135,7 +190,7 @@ fn looksLikeCompletionKind(v: Value) bool {
135190
return n >= 0 and n <= @as(f64, @floatFromInt(@intFromEnum(Completion.continue_)));
136191
}
137192

138-
fn completionKindBelowTop(stack: *const std.ArrayListUnmanaged(Value)) ?Completion {
193+
fn completionKindBelowTop(stack: *const OperandStack) ?Completion {
139194
if (stack.items.len == 0) return null;
140195
const v = stack.items[stack.items.len - 1];
141196
if (!looksLikeCompletionKind(v)) return null;

0 commit comments

Comments
 (0)