Skip to content

Commit 87771c9

Browse files
Refactor(VM): Introduce OpcodeCache with hot-path rewriting and HeapPool slot migration.
1 parent 45461e8 commit 87771c9

File tree

4 files changed

+188
-194
lines changed

4 files changed

+188
-194
lines changed

compiler/src/modules/vm/cache.rs

Lines changed: 50 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// vm/cache.rs
2-
3-
use super::types::{Val, DictMap};
2+
use super::types::{Val, eq_vals_deep};
43
use crate::modules::parser::OpCode;
54
use alloc::{vec, vec::Vec};
65
use hashbrown::HashMap;
@@ -20,41 +19,65 @@ pub enum FastOp {
2019
}
2120

2221
/*
23-
Inline Cache
24-
Per-frame type pair recorder that promotes stable ops after threshold hits.
22+
Opcode Cache
23+
Per-frame slot combining inline-cache type recording with adaptive hot-path rewriting. Collocates both tiers per instruction to avoid split cache lines.
2524
*/
2625

2726
const CACHE_THRESH: u8 = 8;
27+
const HOT_THRESH: u32 = 1_000;
2828

2929
#[derive(Clone)]
30-
struct Slot { hits: u8, ta: u8, tb: u8, fast: Option<FastOp> }
31-
impl Slot { fn empty() -> Self { Self { hits: 0, ta: 0, tb: 0, fast: None } } }
30+
struct CacheSlot {
31+
ic_hits: u8, ta: u8, tb: u8,
32+
ic_fast: Option<FastOp>,
33+
hot_count: u32,
34+
hot_fast: Option<FastOp>,
35+
}
36+
impl CacheSlot { fn empty() -> Self { Self { ic_hits: 0, ta: 0, tb: 0, ic_fast: None, hot_count: 0, hot_fast: None } } }
3237

33-
pub struct InlineCache { slots: Vec<Slot> }
38+
pub struct OpcodeCache { slots: Vec<CacheSlot> }
3439

35-
impl InlineCache {
36-
pub fn new(n: usize) -> Self { Self { slots: vec![Slot::empty(); n] } }
40+
impl OpcodeCache {
41+
pub fn new(n: usize) -> Self { Self { slots: vec![CacheSlot::empty(); n] } }
3742

38-
pub fn record(&mut self, ip: usize, opcode: &OpCode, ta: u8, tb: u8) -> Option<FastOp> {
39-
let s = self.slots.get_mut(ip)?;
43+
pub fn record(&mut self, ip: usize, opcode: &OpCode, ta: u8, tb: u8) {
44+
let Some(s) = self.slots.get_mut(ip) else { return };
4045
if s.ta == ta && s.tb == tb {
41-
s.hits = s.hits.saturating_add(1);
42-
if s.hits >= CACHE_THRESH && s.fast.is_none() {
43-
s.fast = match (opcode, ta, tb) {
44-
(OpCode::Add, 1, 1) => Some(FastOp::AddInt), (OpCode::Add, 2, 2) => Some(FastOp::AddFloat),
45-
(OpCode::Add, 5, 5) => Some(FastOp::AddStr), (OpCode::Sub, 1, 1) => Some(FastOp::SubInt),
46-
(OpCode::Sub, 2, 2) => Some(FastOp::SubFloat), (OpCode::Mul, 1, 1) => Some(FastOp::MulInt),
47-
(OpCode::Mul, 2, 2) => Some(FastOp::MulFloat), (OpCode::Lt, 1, 1) => Some(FastOp::LtInt),
48-
(OpCode::Lt, 2, 2) => Some(FastOp::LtFloat), (OpCode::Eq, 1, 1) => Some(FastOp::EqInt),
49-
(OpCode::Eq, 5, 5) => Some(FastOp::EqStr), _ => None,
50-
};
46+
s.ic_hits = s.ic_hits.saturating_add(1);
47+
if s.ic_hits >= CACHE_THRESH && s.ic_fast.is_none() {
48+
s.ic_fast = Self::specialize(opcode, ta, tb);
49+
}
50+
if s.ic_fast.is_some() {
51+
s.hot_count += 1;
52+
if s.hot_count == HOT_THRESH { s.hot_fast = s.ic_fast; }
5153
}
52-
} else { *s = Slot { hits: 1, ta, tb, fast: None }; }
53-
s.fast
54+
} else {
55+
*s = CacheSlot { ta, tb, ic_hits: 1, ..CacheSlot::empty() };
56+
}
57+
}
58+
59+
#[inline] pub fn get_fast(&self, ip: usize) -> Option<FastOp> {
60+
self.slots.get(ip).and_then(|s| s.hot_fast.or(s.ic_fast))
61+
}
62+
63+
pub fn invalidate(&mut self, ip: usize) {
64+
if let Some(s) = self.slots.get_mut(ip) { *s = CacheSlot::empty(); }
65+
}
66+
67+
pub fn specialized_count(&self) -> usize {
68+
self.slots.iter().filter(|s| s.hot_fast.is_some()).count()
5469
}
5570

56-
pub fn get(&self, ip: usize) -> Option<FastOp> { self.slots.get(ip).and_then(|s| s.fast) }
57-
pub fn invalidate(&mut self, ip: usize) { if let Some(s) = self.slots.get_mut(ip) { *s = Slot::empty(); } }
71+
fn specialize(opcode: &OpCode, ta: u8, tb: u8) -> Option<FastOp> {
72+
match (opcode, ta, tb) {
73+
(OpCode::Add, 1, 1) => Some(FastOp::AddInt), (OpCode::Add, 2, 2) => Some(FastOp::AddFloat),
74+
(OpCode::Add, 5, 5) => Some(FastOp::AddStr), (OpCode::Sub, 1, 1) => Some(FastOp::SubInt),
75+
(OpCode::Sub, 2, 2) => Some(FastOp::SubFloat), (OpCode::Mul, 1, 1) => Some(FastOp::MulInt),
76+
(OpCode::Mul, 2, 2) => Some(FastOp::MulFloat), (OpCode::Lt, 1, 1) => Some(FastOp::LtInt),
77+
(OpCode::Lt, 2, 2) => Some(FastOp::LtFloat), (OpCode::Eq, 1, 1) => Some(FastOp::EqInt),
78+
(OpCode::Eq, 5, 5) => Some(FastOp::EqStr), _ => None,
79+
}
80+
}
5881
}
5982

6083
/*
@@ -66,47 +89,17 @@ const TPL_THRESH: u32 = 4;
6689

6790
struct TplEntry { args: Vec<Val>, result: Val, hits: u32 }
6891

69-
fn eq_vals_heap(a: Val, b: Val, heap: &super::types::HeapPool) -> bool {
70-
use super::types::HeapObj;
71-
if !a.is_heap() || !b.is_heap() { return a.0 == b.0; }
72-
// Content-based Val comparison for cache lookups, recursing into collections.
73-
match (heap.get(a), heap.get(b)) {
74-
(HeapObj::BigInt(x), HeapObj::BigInt(y)) => x.cmp(y) == core::cmp::Ordering::Equal,
75-
(HeapObj::Str(x), HeapObj::Str(y)) => x == y,
76-
(HeapObj::Tuple(x), HeapObj::Tuple(y)) => eq_seq(x, y, |a,b| eq_vals_heap(a,b,heap)),
77-
(HeapObj::List(x), HeapObj::List(y)) => eq_seq(&x.borrow(), &y.borrow(), |a,b| eq_vals_heap(a,b,heap)),
78-
(HeapObj::Set(x), HeapObj::Set(y)) => eq_set(&x.borrow(), &y.borrow(), |a,b| eq_vals_heap(a,b,heap)),
79-
(HeapObj::Dict(x), HeapObj::Dict(y)) => eq_dict(&x.borrow(), &y.borrow(), |a,b| eq_vals_heap(a,b,heap)),
80-
_ => false
81-
}
82-
}
83-
84-
pub(super) fn eq_seq(a: &[Val], b: &[Val], eq: impl Fn(Val,Val)->bool) -> bool {
85-
a.len() == b.len() && a.iter().zip(b).all(|(x,y)| eq(*x,*y))
86-
}
87-
pub(super) fn eq_set(a: &[Val], b: &[Val], eq: impl Fn(Val,Val)->bool) -> bool {
88-
a.len() == b.len() && a.iter().all(|x| b.iter().any(|y| eq(*x,*y)))
89-
}
90-
pub(super) fn eq_dict(a: &DictMap, b: &DictMap, eq: impl Fn(Val,Val)->bool) -> bool {
91-
a.len() == b.len() && a.iter().all(|(k,v)| b.get(k).map_or(false, |v2| eq(*v, *v2)))
92-
}
93-
9492
pub struct Templates { map: HashMap<usize, Vec<TplEntry>> }
9593

9694
impl Templates {
9795
pub fn new() -> Self { Self { map: HashMap::new() } }
9896

99-
/*
100-
Value-Equality Lookup
101-
Finds cached result by content comparison instead of pointer identity.
102-
*/
103-
10497
pub fn lookup(&self, fi: usize, args: &[Val], heap: &super::types::HeapPool) -> Option<Val> {
10598
self.map.get(&fi)?.iter()
10699
.find(|e| {
107100
e.hits >= TPL_THRESH
108101
&& e.args.len() == args.len()
109-
&& e.args.iter().zip(args).all(|(a, b)| eq_vals_heap(*a, *b, heap))
102+
&& e.args.iter().zip(args).all(|(a, b)| eq_vals_deep(*a, *b, heap))
110103
})
111104
.map(|e| e.result)
112105
}
@@ -115,7 +108,7 @@ impl Templates {
115108
let v = self.map.entry(fi).or_default();
116109
if let Some(e) = v.iter_mut().find(|e| {
117110
e.args.len() == args.len()
118-
&& e.args.iter().zip(args).all(|(a, b)| eq_vals_heap(*a, *b, heap))
111+
&& e.args.iter().zip(args).all(|(a, b)| eq_vals_deep(*a, *b, heap))
119112
}) {
120113
e.hits += 1; e.result = result;
121114
} else if v.len() < 256 {
@@ -126,29 +119,4 @@ impl Templates {
126119
pub fn count(&self) -> usize {
127120
self.map.values().flat_map(|v| v.iter()).filter(|e| e.hits >= TPL_THRESH).count()
128121
}
129-
}
130-
131-
/*
132-
Adaptive Engine
133-
Rewrites hot instructions with specialized overlays after one thousand executions.
134-
*/
135-
136-
const HOT_THRESH: u32 = 1_000;
137-
138-
pub struct Adaptive { counts: Vec<u32>, overlay: Vec<Option<FastOp>> }
139-
140-
impl Adaptive {
141-
pub fn new(n: usize) -> Self { Self { counts: vec![0; n], overlay: vec![None; n] } }
142-
pub fn tick(&mut self, ip: usize) -> bool {
143-
if let Some(c) = self.counts.get_mut(ip) { *c += 1; *c == HOT_THRESH } else { false }
144-
}
145-
pub fn rewrite(&mut self, ip: usize, f: FastOp) {
146-
if let Some(s) = self.overlay.get_mut(ip) { *s = Some(f); }
147-
}
148-
pub fn get(&self, ip: usize) -> Option<FastOp> { self.overlay.get(ip).and_then(|o| *o) }
149-
pub fn deopt(&mut self, ip: usize) {
150-
if let Some(s) = self.overlay.get_mut(ip) { *s = None; }
151-
if let Some(c) = self.counts.get_mut(ip) { *c = 0; }
152-
}
153-
pub fn count(&self) -> usize { self.overlay.iter().filter(|o| o.is_some()).count() }
154122
}

compiler/src/modules/vm/mod.rs

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ mod collections;
99
pub use types::{Val, HeapObj, HeapPool, VmErr, Limits};
1010

1111
use types::*;
12-
use cache::*;
12+
use cache::{OpcodeCache, FastOp, Templates};
1313
use ops::cached_binop;
1414

1515
use crate::modules::parser::{OpCode, SSAChunk, Value, BUILTIN_TYPES};
@@ -79,7 +79,7 @@ impl<'a> VM<'a> {
7979
HeapObj::Range(s, e, st) => IterFrame::Range { cur: *s, end: *e, step: *st },
8080
HeapObj::List(v) => IterFrame::Seq { items: v.borrow().clone(), idx: 0 },
8181
HeapObj::Tuple(v) => IterFrame::Seq { items: v.clone(), idx: 0 },
82-
HeapObj::Dict(p) => IterFrame::Seq { items: p.borrow().keys().copied().collect(), idx: 0 },
82+
HeapObj::Dict(p) => IterFrame::Seq { items: p.borrow().keys().collect(), idx: 0 },
8383
HeapObj::Set(s) => IterFrame::Seq { items: s.borrow().clone(), idx: 0 },
8484
HeapObj::Str(s) => {
8585
let chars: Vec<char> = s.chars().collect();
@@ -279,13 +279,13 @@ impl<'a> VM<'a> {
279279
Fetches instructions by IP, routes each opcode to its handler arm.
280280
*/
281281

282-
pub(crate) fn exec(&mut self, chunk: &SSAChunk, slots: &mut Vec<Option<Val>>) -> Result<Val, VmErr> {
282+
pub(crate) fn exec(&mut self, chunk: &SSAChunk, slots: &mut Vec<Option<Val>>) -> Result<Val, VmErr> {
283283
let slots_base = self.live_slots.len();
284284
let n = chunk.instructions.len();
285285

286-
// Box per-frame caches to reduce stack frame size in debug builds
287-
let mut cache = Box::new(InlineCache::new(n));
288-
let mut adaptive = Box::new(Adaptive::new(n));
286+
// Box per-frame cache to reduce stack frame size in debug builds
287+
let mut cache = Box::new(OpcodeCache::new(n));
288+
289289
let mut ip = 0usize;
290290

291291
let mut phi_map = vec![0usize; n];
@@ -299,17 +299,13 @@ impl<'a> VM<'a> {
299299
}
300300
}
301301

302-
let prev_slots = &chunk.prev_slots; // SSA alias table: pre-computed in SSAChunk, maps each versioned slot to its predecessor.
302+
let prev_slots = &chunk.prev_slots; // SSA alias table
303303

304304
loop {
305305
if ip >= n { return Ok(Val::none()); }
306306

307-
// Adaptive / inline cache fast paths
308-
if let Some(fast) = adaptive.get(ip) {
309-
ip += 1;
310-
if self.exec_fast(fast)? { continue; }
311-
adaptive.deopt(ip - 1); cache.invalidate(ip - 1); ip -= 1;
312-
} else if let Some(fast) = cache.get(ip) {
307+
// Fast path: hot adaptive overlay first, then inline cache promotion
308+
if let Some(fast) = cache.get_fast(ip) {
313309
ip += 1;
314310
if self.exec_fast(fast)? { continue; }
315311
cache.invalidate(ip - 1); ip -= 1;
@@ -327,7 +323,6 @@ impl<'a> VM<'a> {
327323
match ins.opcode {
328324

329325
// Loads
330-
331326
OpCode::LoadConst => { let v = self.to_val(&chunk.constants[op as usize])?; self.push(v); }
332327
OpCode::LoadName => { let slot = op as usize; self.push(slots[slot].ok_or_else(|| VmErr::Name(chunk.names[slot].clone()))?); }
333328
OpCode::StoreName => {
@@ -337,7 +332,7 @@ impl<'a> VM<'a> {
337332
if let Some(prev) = prev_slots[slot] { slots[prev as usize] = Some(v); }
338333

339334
if self.heap.needs_gc() {
340-
self.collect(slots); // Garbage collector safepoint; store is the only opcode that grows the heap unboundedly
335+
self.collect(slots);
341336
}
342337
}
343338
OpCode::LoadTrue => self.push(Val::bool(true)),
@@ -346,20 +341,20 @@ impl<'a> VM<'a> {
346341
OpCode::LoadEllipsis => { let v = self.heap.alloc(HeapObj::Str("...".into()))?; self.push(v); }
347342

348343
// Arithmetic (cached)
349-
350-
OpCode::Add => { let (a, b) = self.pop2()?; cached_binop!(self.heap, rip, &ins.opcode, a, b, cache, adaptive); let v = self.add_vals(a, b)?; self.push(v); }
351-
OpCode::Sub => { let (a, b) = self.pop2()?; cached_binop!(self.heap, rip, &ins.opcode, a, b, cache, adaptive); let v = self.sub_vals(a, b)?; self.push(v); }
352-
OpCode::Mul => { let (a, b) = self.pop2()?; cached_binop!(self.heap, rip, &ins.opcode, a, b, cache, adaptive); let v = self.mul_vals(a, b)?; self.push(v); }
344+
OpCode::Add => { let (a, b) = self.pop2()?; cached_binop!(self.heap, rip, &ins.opcode, a, b, cache); let v = self.add_vals(a, b)?; self.push(v); }
345+
OpCode::Sub => { let (a, b) = self.pop2()?; cached_binop!(self.heap, rip, &ins.opcode, a, b, cache); let v = self.sub_vals(a, b)?; self.push(v); }
346+
OpCode::Mul => { let (a, b) = self.pop2()?; cached_binop!(self.heap, rip, &ins.opcode, a, b, cache); let v = self.mul_vals(a, b)?; self.push(v); }
353347
OpCode::Div => { let (a, b) = self.pop2()?; let v = self.div_vals(a, b)?; self.push(v); }
348+
354349
OpCode::Mod => {
355-
let (a, b) = self.pop2()?;
356-
if let (Some(ba), Some(bb)) = (self.to_bigint(a), self.to_bigint(b)) {
357-
let (_, r) = ba.divmod(&bb).ok_or(VmErr::ZeroDiv)?;
358-
let v = self.bigint_to_val(r)?;
359-
self.push(v);
360-
} else {
361-
return Err(VmErr::Type("mod requires int".into()));
362-
}
350+
let (a, b) = self.pop2()?;
351+
if let (Some(ba), Some(bb)) = (self.to_bigint(a), self.to_bigint(b)) {
352+
let (_, r) = ba.divmod(&bb).ok_or(VmErr::ZeroDiv)?;
353+
let v = self.bigint_to_val(r)?;
354+
self.push(v);
355+
} else {
356+
return Err(VmErr::Type("mod requires int".into()));
357+
}
363358
}
364359
OpCode::Pow => {
365360
let (a, b) = self.pop2()?;
@@ -423,9 +418,9 @@ impl<'a> VM<'a> {
423418

424419
// Comparison (cached)
425420

426-
OpCode::Eq => { let (a, b) = self.pop2()?; cached_binop!(self.heap, rip, &ins.opcode, a, b, cache, adaptive); self.push(Val::bool(self.eq_vals(a, b))); }
421+
OpCode::Eq => { let (a, b) = self.pop2()?; cached_binop!(self.heap, rip, &ins.opcode, a, b, cache); self.push(Val::bool(self.eq_vals(a, b))); }
422+
OpCode::Lt => { let (a, b) = self.pop2()?; cached_binop!(self.heap, rip, &ins.opcode, a, b, cache); let r = self.lt_vals(a, b)?; self.push(Val::bool(r)); }
427423
OpCode::NotEq => { let (a,b) = self.pop2()?; self.push(Val::bool(!self.eq_vals(a,b))); }
428-
OpCode::Lt => { let (a, b) = self.pop2()?; cached_binop!(self.heap, rip, &ins.opcode, a, b, cache, adaptive); let r = self.lt_vals(a, b)?; self.push(Val::bool(r)); }
429424
OpCode::Gt => { let (a,b) = self.pop2()?; let r=self.lt_vals(b,a)?; self.push(Val::bool(r)); }
430425
OpCode::LtEq => { let (a,b) = self.pop2()?; let r=self.lt_vals(b,a)?; self.push(Val::bool(!r)); }
431426
OpCode::GtEq => { let (a,b) = self.pop2()?; let r=self.lt_vals(a,b)?; self.push(Val::bool(!r)); }

0 commit comments

Comments
 (0)