perf: 压帧时固化 callee upvalue 表,热路径免每指令重查

This commit is contained in:
2026-09-28 17:24:10 +00:00
parent 597f11e802
commit b3255faa46
12 changed files with 110 additions and 88 deletions
+5
View File
@@ -504,6 +504,8 @@ impl JsObject {
/// 深拷贝本对象到 session epoch:复制属性向量与元数据,标记为新 session 对象。
///
/// 用于把持久对象快照进当前调用上下文,修改不反向传播到源对象。
/// `upvalues` 指针别名共享(非深拷):调用帧固化的 upvalue 表跨 GC 晋升
/// 恒有效依赖此不变量。
pub fn clone_for_session_epoch(&self) -> Self {
let hash_props = self
.hash_props_vec()
@@ -626,6 +628,9 @@ impl JsObject {
}
/// 替换 upvalue cell 列表(释放旧列表)。
///
/// 仅闭包创建时调用一次;调用帧在压帧时固化的 upvalue 表(`CallFrame.upvalues`)
/// 跨 GC 恒有效以"本 Box 创建后不替换"为前提。
pub fn set_upvalues(&mut self, v: Box<Vec<*mut Cell>>) {
if !self.upvalues.is_null() {
unsafe {
+11 -36
View File
@@ -384,39 +384,21 @@ impl Vm {
/// 读取当前闭包的 upvalue:命中 cell 判初始化后取值,未命中委托惰性建 cell。
///
/// # 步骤
/// 1. 取 imm16 为 upvalue 下标;缓存命中(键 = 当前 callee 位级相等)直接
/// 解引用缓存切片指针,未命中从 callee 重算并填缓存。
/// 1. 取 imm16 为 upvalue 下标;直接索引活动镜像表(当前执行函数的表,
/// 非闭包为 null 空切片)。
/// 2. 命中且非空:未初始化(TDZ)抛 ReferenceError,否则值写入 `regs[rd]`。
/// 3. 未命中/空槽(`CREATE_CLOSURE` 早于 `MAKE_CELL`):委托
/// `lazy_create_upvalue_cell` 建 cell。
///
/// # 边界与前提
/// - `uv_idx` 越界或当前无 callee 时同样走惰性路径,最终回退 undefined。
/// - 缓存指针在键匹配期间恒有效:callee 对象是 GC 根,`upvalues` Box
/// - 镜像指针跨 GC 恒有效:callee 对象是 GC 根,`upvalues` Box
/// 创建后不替换。
pub(crate) fn dispatch_load_upvalue(&mut self, rd: usize, instr: u32) -> Result<(), String> {
let uv_idx = opcode::imm16(instr) as usize;
let callee = self.current_callee();
// 命中:键位级相等保证缓存切片指针对应当前 callee 对象(对象为 GC 根、
// Box 创建后不替换),直接解引用安全。
let mut upvals: Option<&[*mut Cell]> = None;
if let Some((key, ptr)) = self.upvalue_cache {
if callee == Some(key) {
upvals = Some(unsafe { &*ptr });
}
}
// 未命中:重算切片指针并填缓存(含空切片,免每次 null 判定)。
if upvals.is_none() {
if let Some(callee) = callee {
if callee.is_object() {
let obj = unsafe { &*callee.as_js_object_ptr() };
let slice = obj.upvalues_slice();
self.upvalue_cache = Some((callee, slice as *const [*mut Cell]));
upvals = Some(slice);
}
}
}
if let Some(upvals) = upvals {
// 活动镜像是当前执行函数的表(压帧/内联/弹帧/挂起恢复边界置位):
// 直接解引用安全(callee 是 GC 根、Box 创建后不替换)。
let upvals = unsafe { &*self.active_upvalues };
if uv_idx < upvals.len() {
let cell = upvals[uv_idx];
if !cell.is_null() {
@@ -428,7 +410,6 @@ impl Vm {
return Ok(());
}
}
}
// cell 尚未创建(hoisting 顺序:CREATE_CLOSURE 先于 MAKE_CELL),
// 用调用方寄存器值惰性创建 cell。
self.lazy_create_upvalue_cell(rd, uv_idx)
@@ -470,7 +451,7 @@ impl Vm {
///
/// # 步骤
/// 1. b 槽为 upvalue 下标,源值为 `regs[a]`;`rd` 复用为 const 标志。
/// 2. 命中闭包 `upvalues` 且非空:未初始化(TDZ)抛 ReferenceError,const 标志非 0
/// 2. 命中活动镜像表且非空:未初始化(TDZ)抛 ReferenceError,const 标志非 0
/// 抛 TypeError,否则写值置 initialized。
/// 3. 槽位为空(尚未建 cell)以源值新建已初始化 cell;下标越界或无 callee 静默返回。
///
@@ -484,20 +465,16 @@ impl Vm {
let const_flag = rd;
let uv_idx = b;
let src_val = self.regs[a];
if let Some(callee) = self.current_callee() {
if callee.is_object() {
let obj = unsafe { &mut *callee.as_js_object_ptr() };
let upvals = obj.upvalues_slice_mut();
// 活动镜像是当前执行函数的表(非闭包为 null 空切片):经 *mut 转可变
// 切片,与 upvalues_slice_mut 同口径(callee 是 GC 根、Box 单所有权)。
let upvals = unsafe { &mut *(self.active_upvalues as *mut [*mut Cell]) };
if uv_idx < upvals.len() {
if !upvals[uv_idx].is_null() {
unsafe {
let cell = &mut *upvals[uv_idx];
// TDZ 写检查:cell 未初始化(捕获绑定声明点前)写抛 ReferenceError。
if !cell.is_initialized() {
self.raise_error_kind(
"ReferenceError",
"Cannot access variable before initialization",
)?;
self.raise_error_kind("ReferenceError", "Cannot access variable before initialization")?;
return Ok(());
}
// const guard:TDZ 检查后 cell 必已初始化(存在性判定,与值无关),
@@ -514,8 +491,6 @@ impl Vm {
upvals[uv_idx] = self.gc_state.alloc_cell(src_val, true);
}
}
}
}
Ok(())
}
}
+1
View File
@@ -107,6 +107,7 @@ fn gc_roots_contains_registers_frames_and_root_roots() {
is_derived_constructor: false,
super_called: false,
continuation: FrameContinuation::None,
upvalues: std::ptr::slice_from_raw_parts(std::ptr::null(), 0),
});
vm.save_stack.push(JsValue::from_js_object(frame_session));
+11 -3
View File
@@ -83,9 +83,8 @@ impl SuspendedFrame {
pub fn save_from(&mut self, vm: &mut Vm, callee: JsValue) -> Result<(), String> {
let frame = vm.frames.pop().ok_or_else(|| "frame missing on suspend".to_string())?;
self.frame = Some(frame);
// 挂起弹帧不经 restore_frame:失效 upvalue 切片缓存(值相等键已自愈,
// 此为双保险)。
vm.upvalue_cache = None;
// 挂起弹帧不经 restore_frame:帧已弹入状态盒,活动镜像清零(null 空切片)。
vm.active_upvalues = std::ptr::slice_from_raw_parts(std::ptr::null(), 0);
*self.regs = vm.regs;
self.pc = vm.pc;
self.bytecode = std::mem::take(&mut vm.bytecode);
@@ -150,9 +149,16 @@ impl SuspendedFrame {
vm.active_reg_limit = self.active_reg_limit;
vm.root_reg_limit = self.root_reg_limit;
vm.frames.clear();
// 帧弹回前取被恢复函数的表,弹回后置回活动镜像(帧空即顶层空切片)。
let up = self
.frame
.as_ref()
.map(|f| f.upvalues)
.unwrap_or(std::ptr::slice_from_raw_parts(std::ptr::null(), 0));
if let Some(frame) = self.frame.take() {
vm.frames.push(frame);
}
vm.active_upvalues = up;
vm.spill_stack = std::mem::take(&mut self.spill_stack);
vm.save_stack = std::mem::take(&mut self.save_stack);
vm.cell_stack = std::mem::take(&mut self.cell_stack);
@@ -350,6 +356,7 @@ impl SuspendedFrame {
super_called: f.super_called,
strict: f.strict,
continuation: f.continuation,
upvalues: f.upvalues,
}),
spill_stack: self.spill_stack.iter().copied().map(&mut rewrite).collect(),
save_stack: self.save_stack.iter().copied().map(&mut rewrite).collect(),
@@ -447,6 +454,7 @@ mod tests {
super_called: false,
strict: false,
continuation: FrameContinuation::None,
upvalues: std::ptr::slice_from_raw_parts(std::ptr::null(), 0),
}
}
+7 -3
View File
@@ -4,7 +4,7 @@
use std::sync::Arc;
use oxide_runtime_api::NativeResult;
use oxide_types::object::JsObject;
use oxide_types::object::{Cell, JsObject};
use oxide_types::value::JsValue;
use smallvec::SmallVec;
@@ -301,6 +301,9 @@ impl Vm {
.map(|name| self.kernel_core.perm_interner().intern(name).0)
.unwrap_or(0);
// 压帧时固化 callee 的 upvalue 表(非闭包为 null 空切片):热路径免
// 每指令重查 callee 对象,指针跨 GC 恒有效(callee 是 GC 根)。
let up = obj.upvalues_slice() as *const [*mut Cell];
self.frames.push(CallFrame {
return_addr: self.pc,
function_name,
@@ -319,9 +322,10 @@ impl Vm {
super_called: false,
strict: sub_is_strict,
continuation,
upvalues: up,
});
// 压帧后 callee 换为被调函数:失效 upvalue 切片缓存。
self.upvalue_cache = None;
// 压帧后活动镜像切到被调函数的表(非闭包为 null 空切片)。
self.active_upvalues = up;
self.pc = 0;
self.bytecode = sub_bytecode;
+8
View File
@@ -1,6 +1,7 @@
//! 调用帧与完成值类型:帧续行方式、压帧实参来源、调用帧、for-in 迭代游标、
//! try/catch/finally 处理记录,以及 break/continue/return 完成值(含访问器)。
use oxide_types::object::Cell;
use oxide_types::value::JsValue;
/// 调用帧被挂起后,恢复时需要继续的执行方式。
@@ -75,6 +76,13 @@ pub struct CallFrame {
/// 属性写失败时据此分派:严格模式抛 TypeError,sloppy 静默 no-op。
pub strict: bool,
pub continuation: FrameContinuation,
/// 被调函数 upvalue cell 表(压帧时从 callee 对象固化,非闭包为 null 空切片)。
///
/// SAFETY 前提:`callee` 是 GC 根(根遍历/重写/深拷贝三处均遍历该字段),
/// 其 `upvalues` Box 创建后不替换(`set_upvalues` 仅 CREATE_CLOSURE 调用一次,
/// 晋升克隆共享同一 Box),故指针跨 GC 恒有效;写路径经 `*mut` 转可变切片
/// 与 `upvalues_slice_mut` 同口径(Box 单所有权)。
pub upvalues: *const [*mut Cell],
}
/// 一次 `for-in` 迭代的游标:已收集的 key 列表与当前下标。
+3 -2
View File
@@ -40,8 +40,9 @@ pub(crate) struct InlineSyncState {
pub(crate) spill_stack: Vec<JsValue>,
pub(crate) cell_stack: Vec<Vec<*mut Cell>>,
pub(crate) inline_callee: Option<JsValue>,
/// upvalue 切片指针缓存(与 `inline_callee` 同生命周期,随本快照保存/恢复)。
pub(crate) upvalue_cache: Option<(JsValue, *const [*mut Cell])>,
/// 当前执行函数的 upvalue cell 表镜像(与 `inline_callee` 同生命周期,
/// 随本快照保存/恢复)。
pub(crate) active_upvalues: *const [*mut Cell],
/// 三个内嵌 dispatch 调度标志(`Vm::generator_dispatch` / `async_dispatch` /
/// `construct_dispatch`)的属主快照。save 时记录外层值并清零 VM 侧、restore
/// 时写回:嵌套 state-swap 调用不得继承外层调度上下文,否则其内部构造帧
+5 -5
View File
@@ -270,11 +270,11 @@ pub struct Vm {
/// frames 为空(inline 隔离状态)时由此取闭包 upvalues。嵌套 inline 由
/// InlineSyncState 保存/恢复。
pub(crate) inline_callee: Option<JsValue>,
/// upvalue 切片指针缓存:键 = 当前 callee 值(位级相等),值 = 该 callee 的
/// `upvalues` 切片指针(含空切片情形)。LOAD_UPVALUE 命中时省 `is_object` +
/// 对象解引用 + 切片解引用。压帧/弹帧/挂起/run 边界失效;键匹配期间指针恒
/// 有效——callee 对象是 GC 根,`upvalues` Box 创建后不替换。
pub(crate) upvalue_cache: Option<(JsValue, *const [*mut Cell])>,
/// 当前执行函数的 upvalue cell 表(活动镜像,与 `CallFrame.upvalues` 同型:
/// 胖指针,非闭包为 null 空切片)。压帧/内联/弹帧/挂起恢复四个边界置位,
/// run 边界清零;指针恒有效——callee 对象是 GC 根,`upvalues` Box 创建后不
/// 替换,LOAD/STORE_UPVALUE 热路径直接按下标取 cell。
pub(crate) active_upvalues: *const [*mut Cell],
/// inline 目标函数的严格模式标志(与 `inline_callee` 同生命周期,
/// InlineSyncState 保存/恢复):内联执行期间帧表被隔离,写路径的
/// strict/sloppy 判定取此值而非帧标志。
+14 -8
View File
@@ -4,7 +4,7 @@ use oxide_bytecode::module::CompiledModule;
use crate::vm::{CallFrame, FrameArgs, FrameContinuation, InlineSyncState, TableGen, Vm};
use crate::{vm_debug, vm_info, vm_trace, vm_warn};
use oxide_types::object::JsObject;
use oxide_types::object::{Cell, JsObject};
use oxide_types::value::JsValue;
/// 按 `flat_id` 下标收集整棵子模块树为平表,供 `run()` 装载。
@@ -101,8 +101,8 @@ macro_rules! inline_save_field {
($recv:ident, $window_regs:ident, inline_callee, opt_copy) => {
$recv.inline_callee
};
($recv:ident, $window_regs:ident, upvalue_cache, copy) => {
$recv.upvalue_cache
($recv:ident, $window_regs:ident, active_upvalues, copy) => {
$recv.active_upvalues
};
($recv:ident, $window_regs:ident, generator_dispatch, flag_zero) => {{
let prev = $recv.generator_dispatch;
@@ -211,8 +211,8 @@ macro_rules! inline_restore_field {
($recv:ident, $saved:ident, inline_callee, opt_copy) => {
$recv.inline_callee = $saved.inline_callee
};
($recv:ident, $saved:ident, upvalue_cache, copy) => {
$recv.upvalue_cache = $saved.upvalue_cache
($recv:ident, $saved:ident, active_upvalues, copy) => {
$recv.active_upvalues = $saved.active_upvalues
};
($recv:ident, $saved:ident, generator_dispatch, flag_zero) => {
$recv.generator_dispatch = $saved.generator_dispatch
@@ -277,7 +277,7 @@ macro_rules! inline_core_fields {
(spill_stack, move_field), // V M
(cell_stack, move_field), // V M
(inline_callee, opt_copy), // V M
(upvalue_cache, copy), // V
(active_upvalues, copy), // M
(generator_dispatch, flag_zero), // M 调度标志:快照属主值,嵌套期间清零
(async_dispatch, flag_zero), // M 调度标志:快照属主值,嵌套期间清零
(construct_dispatch, flag_zero), // M 调度标志:快照属主值,嵌套期间清零
@@ -477,6 +477,8 @@ impl Vm {
self.root_reg_limit = self.active_reg_limit;
self.cell_stack.push(Vec::with_capacity(sub.cells_needed as usize));
self.inline_callee = Some(callee);
// 内联无帧:活动镜像直接取被调对象表(非闭包为 null 空切片)。
self.active_upvalues = callee_obj.upvalues_slice() as *const [*mut Cell];
// inline 无 CallFrame:目标函数严格模式单独记录,内联执行期间的写路径
// strict/sloppy 判定据此分派(嵌套内联时外层值由 InlineSyncState 恢复)。
self.inline_strict = sub.is_strict;
@@ -550,8 +552,12 @@ impl Vm {
callee_mut.set_prop_at(slot, JsValue::undefined());
}
}
// 弹帧后 callee 换回调用方:失效 upvalue 切片缓存。
self.upvalue_cache = None;
// 弹帧后活动镜像切回新栈顶的表(帧空即顶层空切片)。
self.active_upvalues = self
.frames
.last()
.map(|f| f.upvalues)
.unwrap_or(std::ptr::slice_from_raw_parts(std::ptr::null(), 0));
if let Some(saved_bc) = self.saved_bytecode_stack.pop() {
self.bytecode = saved_bc;
}
+5 -5
View File
@@ -101,7 +101,7 @@ impl Vm {
inline_args_count: 0,
accessor_frame_target_reg: None,
inline_callee: None,
upvalue_cache: None,
active_upvalues: std::ptr::slice_from_raw_parts(std::ptr::null(), 0),
inline_strict: false,
inline_frames_base: 0,
top_level_strict: false,
@@ -239,7 +239,7 @@ impl Vm {
inline_args_count: 0,
accessor_frame_target_reg: None,
inline_callee: None,
upvalue_cache: None,
active_upvalues: std::ptr::slice_from_raw_parts(std::ptr::null(), 0),
inline_strict: false,
inline_frames_base: 0,
top_level_strict: false,
@@ -556,9 +556,9 @@ impl Vm {
self.reentry_hops = 0;
// inline 窗口缓冲池内容为已废弃快照,跨 run/reset 不保留。
self.inline_reg_pool = None;
// upvalue 切片缓存是执行期状态:键为 callee 值,跨 run/reset 后缓存的
// 切片指针失效(callee 对象可能已回收)。
self.upvalue_cache = None;
// upvalue 活动镜像是执行期状态:跨 run/reset 后指针失效(callee 对象
// 可能已回收),清零为 null 空切片。
self.active_upvalues = std::ptr::slice_from_raw_parts(std::ptr::null(), 0);
// 微任务队列是执行期状态:跨 run 不保留。
self.job_queue.clear();
// Atomics waiter 表同属执行期状态:未结算 promise 清位后无强根。
+1
View File
@@ -844,6 +844,7 @@ fn reset_clears_runtime_state_like_rerun() {
is_derived_constructor: false,
super_called: false,
continuation: crate::vm::FrameContinuation::None,
upvalues: std::ptr::slice_from_raw_parts(std::ptr::null(), 0),
});
vm.save_stack.push(JsValue::undefined());
vm.iters
@@ -40,3 +40,16 @@ fn generator_promoted_to_global_survives_full_reset_cleanly() {
.expect("run3");
assert_eq!(format!("{ok}"), "9");
}
/// 闭包跨 yield 读 upvalue:生成器挂起再恢复时活动镜像必须还原为生成器自己的
/// 表(恢复置位漏掉时,恢复体读顶层空表,upvalue 读回退惰性路径得 undefined)。
#[test]
fn generator_reads_upvalue_across_yield() {
let mut vm = Vm::new();
let result = vm
.run(&Arc::new(compile(
"function make() { var x = 1; function* g() { yield x; yield x; } var it = g(); it.next(); x = 2; return it.next().value; } make()",
)))
.expect("run");
assert_eq!(format!("{result}"), "2");
}