perf: reuse numeric VM slots by generation

This commit is contained in:
boris
2026-09-06 05:31:23 +08:00
parent 5f08978827
commit 5122c73aa8
+65 -26
View File
@@ -168,36 +168,40 @@ impl Program {
Instruction::Push(value) => scratch.stack.push(value), Instruction::Push(value) => scratch.stack.push(value),
Instruction::LoadVariable(index) => { Instruction::LoadVariable(index) => {
let index = usize::from(index); let index = usize::from(index);
let cached = scratch.variables[index]; let value = if scratch.variable_generations[index] == scratch.generation {
let value = match cached { scratch.variables[index]
Some(value) => value, } else {
None => { let expected_type = self.variable_types[index];
let expected_type = self.variable_types[index]; let value = resolve(index, &self.variables[index], expected_type)?;
let value = resolve(index, &self.variables[index], expected_type)?; if value.value_type() != expected_type {
if value.value_type() != expected_type { return Err(EvalError::new(format!(
return Err(EvalError::new(format!( "variable {} expected {:?}, got {:?}",
"variable {} expected {:?}, got {:?}", self.variables[index],
self.variables[index], expected_type,
expected_type, value.value_type()
value.value_type() )));
)));
}
scratch.variables[index] = Some(value);
value
} }
scratch.variables[index] = value;
scratch.variable_generations[index] = scratch.generation;
value
}; };
scratch.stack.push(value); scratch.stack.push(value);
} }
Instruction::LoadLocal(index) => { Instruction::LoadLocal(index) => {
let index = usize::from(index); let index = usize::from(index);
let value = scratch.locals[index].ok_or_else(|| { if scratch.local_generations[index] != scratch.generation {
EvalError::new(format!("local slot {index} was not initialized")) return Err(EvalError::new(format!(
})?; "local slot {index} was not initialized"
)));
}
let value = scratch.locals[index];
scratch.stack.push(value); scratch.stack.push(value);
} }
Instruction::StoreLocal(index) => { Instruction::StoreLocal(index) => {
let index = usize::from(index);
let value = pop(&mut scratch.stack)?; let value = pop(&mut scratch.stack)?;
scratch.locals[usize::from(index)] = Some(value); scratch.locals[index] = value;
scratch.local_generations[index] = scratch.generation;
} }
Instruction::Unary(operator) => { Instruction::Unary(operator) => {
let value = pop(&mut scratch.stack)?; let value = pop(&mut scratch.stack)?;
@@ -260,8 +264,11 @@ impl Program {
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub(crate) struct Scratch { pub(crate) struct Scratch {
stack: Vec<Value>, stack: Vec<Value>,
variables: Vec<Option<Value>>, variables: Vec<Value>,
locals: Vec<Option<Value>>, variable_generations: Vec<u32>,
locals: Vec<Value>,
local_generations: Vec<u32>,
generation: u32,
} }
impl Scratch { impl Scratch {
@@ -273,10 +280,21 @@ impl Scratch {
.len() .len()
.saturating_sub(self.stack.capacity()), .saturating_sub(self.stack.capacity()),
); );
self.variables.clear(); self.generation = self.generation.wrapping_add(1);
self.variables.resize(program.variables.len(), None); if self.generation == 0 {
self.locals.clear(); self.variable_generations.fill(0);
self.locals.resize(program.local_count, None); self.local_generations.fill(0);
self.generation = 1;
}
if self.variables.len() < program.variables.len() {
self.variables
.resize(program.variables.len(), Value::Number(0.0));
self.variable_generations.resize(program.variables.len(), 0);
}
if self.locals.len() < program.local_count {
self.locals.resize(program.local_count, Value::Number(0.0));
self.local_generations.resize(program.local_count, 0);
}
} }
} }
@@ -1362,6 +1380,27 @@ mod tests {
} }
} }
#[test]
fn scratch_generation_rollover_invalidates_cached_slots() {
let program = compile("left + right", |_| Some(ValueType::Number)).expect("compile");
let mut scratch = Scratch::default();
let first = program
.evaluate(&mut scratch, |_index, name, _expected| {
Ok(Value::Number(if name == "left" { 1.0 } else { 2.0 }))
})
.expect("first evaluation");
assert_eq!(first, Value::Number(3.0));
scratch.generation = u32::MAX;
let second = program
.evaluate(&mut scratch, |_index, name, _expected| {
Ok(Value::Number(if name == "left" { 10.0 } else { 20.0 }))
})
.expect("evaluation after generation rollover");
assert_eq!(second, Value::Number(30.0));
assert_eq!(scratch.generation, 1);
}
#[test] #[test]
fn matches_rhai_for_representative_numeric_boolean_corpus() { fn matches_rhai_for_representative_numeric_boolean_corpus() {
let source = r#" let source = r#"