Skip to content

Commit c16472f

Browse files
perf(vm): reduce dispatch and call overhead
1 parent 61bf1c0 commit c16472f

9 files changed

Lines changed: 305 additions & 155 deletions

File tree

compiler/src/modules/parser/literals.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,12 @@ impl<'src, I: Iterator<Item = Token>> Parser<'src, I> {
481481
| OpCode::Yield
482482
| OpCode::YieldFrom
483483
));
484+
// Pre-compute generator flag once at parse time. Avoids the
485+
// O(n_instructions) scan that exec_call would otherwise do per call.
486+
body.is_generator = body.instructions.iter().any(|i| matches!(
487+
i.opcode,
488+
OpCode::Yield | OpCode::YieldFrom
489+
));
484490
body
485491
}
486492
}

compiler/src/modules/parser/types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ pub struct SSAChunk {
100100
pub phi_sources: Vec<(u16, u16)>,
101101
pub classes: Vec<SSAChunk>,
102102
pub is_pure: bool,
103+
pub is_generator: bool,
103104
pub overflow: bool,
104105
pub prev_slots: Vec<Option<u16>>,
105106
pub alias_groups: Vec<Vec<u16>>,

compiler/src/modules/vm/cache.rs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// vm/cache.rs
22

3-
use super::types::{Val, eq_vals_with_heap};
4-
use crate::modules::parser::{OpCode, SSAChunk, Instruction};
3+
use super::types::{Val, HeapObj, HeapPool, VmErr, BigInt, eq_vals_with_heap};
4+
use crate::modules::parser::{OpCode, SSAChunk, Instruction, Value};
55
use crate::modules::fx::FxHashMap as HashMap;
66

7-
use alloc::{vec, vec::Vec};
7+
use alloc::{vec, vec::Vec, string::ToString};
88

99
/* Specialized operation types for inline cache type-stable binary dispatch. */
1010

@@ -34,13 +34,18 @@ struct CacheSlot {
3434
pub struct OpcodeCache {
3535
slots: Vec<CacheSlot>,
3636
fused: Option<Vec<Instruction>>,
37+
/// Pre-materialized constant pool. Built once per chunk on first exec, so
38+
/// LoadConst becomes a single indexed load instead of a per-iteration
39+
/// match on `Value` + (for strings/bigints) heap alloc.
40+
const_vals: Option<Vec<Val>>,
3741
}
3842

3943
impl OpcodeCache {
4044
pub fn new(chunk: &SSAChunk) -> Self {
4145
Self {
4246
slots: vec![CacheSlot::default(); chunk.instructions.len()],
4347
fused: None,
48+
const_vals: None,
4449
}
4550
}
4651

@@ -57,6 +62,38 @@ impl OpcodeCache {
5762
self.fused.as_ref().expect("fused code not compiled")
5863
}
5964

65+
/// Materialize the constant pool. Int/Float/Bool/None become inline Vals
66+
/// (no heap touch); Str/BigInt allocate once and are shared. Subsequent
67+
/// LoadConst executions just index into the resulting slice.
68+
pub fn ensure_const_vals(&mut self, chunk: &SSAChunk, heap: &mut HeapPool)
69+
-> Result<&[Val], VmErr>
70+
{
71+
if self.const_vals.is_none() {
72+
let mut out = Vec::with_capacity(chunk.constants.len());
73+
for c in &chunk.constants {
74+
let v = match c {
75+
Value::Int(i) => {
76+
if *i >= Val::INT_MIN && *i <= Val::INT_MAX { Val::int(*i) }
77+
else { heap.alloc(HeapObj::BigInt(BigInt::from_i64(*i)))? }
78+
}
79+
Value::BigInt(s) => heap.alloc(HeapObj::BigInt(BigInt::from_decimal(s)))?,
80+
Value::Float(f) => Val::float(*f),
81+
Value::Bool(b) => Val::bool(*b),
82+
Value::None => Val::none(),
83+
Value::Str(s) => heap.alloc(HeapObj::Str(s.to_string()))?,
84+
};
85+
out.push(v);
86+
}
87+
self.const_vals = Some(out);
88+
}
89+
Ok(self.const_vals.as_ref().unwrap())
90+
}
91+
92+
/// Precomputed constant pool. Caller must have invoked ensure_const_vals.
93+
pub fn const_vals_ref(&self) -> &[Val] {
94+
self.const_vals.as_ref().expect("const pool not materialized")
95+
}
96+
6097
pub fn record(&mut self, ip: usize, opcode: &OpCode, ta: u8, tb: u8) {
6198
let Some(s) = self.slots.get_mut(ip) else { return };
6299
let key = (ta << 4) | (tb & 0xF);

compiler/src/modules/vm/handlers/arith.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,9 @@ impl<'a> VM<'a> {
148148

149149
pub(crate) fn handle_compare(&mut self, op: OpCode, rip: usize, cache: &mut OpcodeCache) -> Result<(), VmErr> {
150150
let (a, b) = self.pop2()?;
151-
if matches!(op, OpCode::Eq | OpCode::Lt) {
152-
cached_binop!(self.heap, rip, &op, a, b, cache);
153-
}
151+
// Record type-key for every comparison op; cache::specialize() decides
152+
// which ones have a FastOp variant. Eq/Lt/NotEq/Gt/LtEq/GtEq all do.
153+
cached_binop!(self.heap, rip, &op, a, b, cache);
154154
let result = match op {
155155
OpCode::Eq => eq_vals_with_heap(a, b, &self.heap),
156156
OpCode::NotEq => !eq_vals_with_heap(a, b, &self.heap),

compiler/src/modules/vm/handlers/data.rs

Lines changed: 57 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ impl<'a> VM<'a> {
66

77
/* StoreName con back-propagación SSA a versiones previas. */
88

9-
pub(crate) fn handle_store(&mut self, operand: u16, slots: &mut [Option<Val>]) -> Result<(), VmErr> {
9+
pub(crate) fn handle_store(&mut self, operand: u16, slots: &mut [Val]) -> Result<(), VmErr> {
1010
let v = self.pop()?;
11-
slots[operand as usize] = Some(v);
11+
slots[operand as usize] = v;
1212
Ok(())
1313
}
1414

@@ -119,15 +119,15 @@ impl<'a> VM<'a> {
119119

120120
/* Side-effects and impurities: assert, del, global/nonlocal, import, type aliases, exception handling stubs and await/yield-from. */
121121

122-
pub(crate) fn handle_side(&mut self, op: OpCode, operand: u16, slots: &mut [Option<Val>]) -> Result<(), VmErr> {
122+
pub(crate) fn handle_side(&mut self, op: OpCode, operand: u16, slots: &mut [Val]) -> Result<(), VmErr> {
123123
match op {
124124
OpCode::Assert => {
125125
let v = self.pop()?;
126126
if !self.truthy(v) { return Err(VmErr::Runtime("AssertionError")); }
127127
}
128128
OpCode::Del => {
129129
let slot = operand as usize;
130-
if slot < slots.len() { slots[slot] = None; }
130+
if slot < slots.len() { slots[slot] = Val::undef(); }
131131
}
132132
OpCode::Global | OpCode::Nonlocal => self.mark_impure(),
133133
OpCode::TypeAlias => { self.pop()?; }
@@ -140,59 +140,59 @@ impl<'a> VM<'a> {
140140
let msg = self.display(exc);
141141
return Err(VmErr::Raised(msg));
142142
}
143-
OpCode::Await => {
144-
// If awaiting a coroutine, run it to completion or yield
145-
let val = self.pop()?;
146-
if val.is_heap() && matches!(self.heap.get(val), HeapObj::Coroutine(..)) {
147-
// Resume the inner coroutine
148-
self.push(val);
149-
// Use Call dispatch with 0 args - callee is on stack
150-
let callee = val;
151-
if let HeapObj::Coroutine(ip, saved_slots, saved_stack, fi, saved_iters) = self.heap.get(callee) {
152-
let (ip, fi) = (*ip, *fi);
153-
let mut fn_slots = saved_slots.clone();
154-
let saved_stack_len = self.stack.len();
155-
let saved_iter_len = self.iter_stack.len();
156-
self.stack.extend_from_slice(&saved_stack.clone());
157-
self.iter_stack.extend(saved_iters.clone());
158-
let saved_yielded = self.yielded;
159-
self.yielded = false;
160-
self.depth += 1;
161-
let (_, body, _, _) = self.functions[fi];
162-
let result = self.exec_from(body, &mut fn_slots, ip);
163-
self.depth -= 1;
164-
let result = result?;
165-
if self.yielded {
166-
// Inner coroutine yielded - propagate yield upward
167-
self.yielded = false;
168-
let resume_ip = self.resume_ip;
169-
let remaining = self.stack.split_off(saved_stack_len);
170-
let coro_iters: Vec<super::super::types::IterFrame> = self.iter_stack.drain(saved_iter_len..).collect();
171-
if let HeapObj::Coroutine(sip, ss, sst, _, si) = self.heap.get_mut(callee) {
172-
*sip = resume_ip;
173-
*ss = fn_slots;
174-
*sst = remaining;
175-
*si = coro_iters;
176-
}
177-
// Propagate: yield the value from this coroutine too
178-
self.push(result);
179-
self.yielded = true;
180-
} else {
181-
// Inner coroutine finished - push its return value
182-
self.stack.truncate(saved_stack_len);
183-
self.iter_stack.truncate(saved_iter_len);
184-
self.yielded = saved_yielded;
185-
self.push(result);
186-
}
187-
} else {
188-
// Not a coroutine anymore (shouldn't happen)
189-
self.push(val);
190-
}
191-
} else {
192-
// Not a coroutine - just push the value (sync call already resolved)
193-
self.push(val);
194-
}
195-
}
143+
OpCode::Await => {
144+
// If awaiting a coroutine, run it to completion or yield
145+
let val = self.pop()?;
146+
if val.is_heap() && matches!(self.heap.get(val), HeapObj::Coroutine(..)) {
147+
// Resume the inner coroutine
148+
self.push(val);
149+
// Use Call dispatch with 0 args - callee is on stack
150+
let callee = val;
151+
if let HeapObj::Coroutine(ip, saved_slots, saved_stack, fi, saved_iters) = self.heap.get(callee) {
152+
let (ip, fi) = (*ip, *fi);
153+
let mut fn_slots = saved_slots.clone();
154+
let saved_stack_len = self.stack.len();
155+
let saved_iter_len = self.iter_stack.len();
156+
self.stack.extend_from_slice(&saved_stack.clone());
157+
self.iter_stack.extend(saved_iters.clone());
158+
let saved_yielded = self.yielded;
159+
self.yielded = false;
160+
self.depth += 1;
161+
let (_, body, _, _) = self.functions[fi];
162+
let result = self.exec_from(body, &mut fn_slots, ip);
163+
self.depth -= 1;
164+
let result = result?;
165+
if self.yielded {
166+
// Inner coroutine yielded - propagate yield upward
167+
self.yielded = false;
168+
let resume_ip = self.resume_ip;
169+
let remaining = self.stack.split_off(saved_stack_len);
170+
let coro_iters: Vec<super::super::types::IterFrame> = self.iter_stack.drain(saved_iter_len..).collect();
171+
if let HeapObj::Coroutine(sip, ss, sst, _, si) = self.heap.get_mut(callee) {
172+
*sip = resume_ip;
173+
*ss = fn_slots;
174+
*sst = remaining;
175+
*si = coro_iters;
176+
}
177+
// Propagate: yield the value from this coroutine too
178+
self.push(result);
179+
self.yielded = true;
180+
} else {
181+
// Inner coroutine finished - push its return value
182+
self.stack.truncate(saved_stack_len);
183+
self.iter_stack.truncate(saved_iter_len);
184+
self.yielded = saved_yielded;
185+
self.push(result);
186+
}
187+
} else {
188+
// Not a coroutine anymore (shouldn't happen)
189+
self.push(val);
190+
}
191+
} else {
192+
// Not a coroutine - just push the value (sync call already resolved)
193+
self.push(val);
194+
}
195+
}
196196
OpCode::YieldFrom => {}
197197
_ => unreachable!("non-side opcode in handle_side"),
198198
}

0 commit comments

Comments
 (0)