perf: 数组元素访问 dispatch 内联 fast path + length_si 缓存
GET_PROP_DYNAMIC/SET_PROP_DYNAMIC 开头内联数组元素 fast path:整数键 + 真数组 + 无元素 meta + 界内时直读/直写元素区,免 primitive_property_get / ordinary_get/set 调用链与 __proto__ 每次 RwLock 查询;越界与有 meta 场景回退原路径(CreateDataProperty + length 扩展 / accessor/hole 协议)。 GET 侧 is_object() 快检先于 checked_object_ptr,保持原始值 receiver 不抛错。length_si 缓存为 Vm 字段(builtin 绑定后取 id,稳定非 0), 普通属性路径 5 处 hot spot 改读缓存,消除每次 intern 的共享锁开销。
This commit is contained in:
@@ -131,7 +131,7 @@ impl Vm {
|
||||
);
|
||||
// 函数名推断:emit 端在变量声明/对象属性赋值点设置 function_name。
|
||||
let func_obj = unsafe { &mut *result.as_js_object_ptr() };
|
||||
let length_si = self.kernel_core.perm_interner().intern("length").0;
|
||||
let length_si = self.length_si;
|
||||
let name_si = self.kernel_core.perm_interner().intern("name").0;
|
||||
// length/name 描述符均为不可写、不可枚举、可配置(SetFunctionLength /
|
||||
// SetFunctionName 语义);length 先于 name 定义保证属性序 [prototype, length, name]。
|
||||
|
||||
@@ -216,7 +216,7 @@ impl Vm {
|
||||
}
|
||||
|
||||
// length:实参个数,可写、不可枚举、可配置。
|
||||
let length_si = self.kernel_core.perm_interner().intern("length").0;
|
||||
let length_si = self.length_si;
|
||||
if let Err(msg) = self.define_data_property(
|
||||
obj,
|
||||
length_si,
|
||||
|
||||
@@ -3,7 +3,7 @@ use oxide_bytecode::opcode::OpCode;
|
||||
use oxide_kernel::prop_forge::PropTemplate;
|
||||
use oxide_runtime_api as coercion;
|
||||
use oxide_types::object::JsObject;
|
||||
use oxide_types::private_key::make_private_name_id;
|
||||
use oxide_types::private_key::{int_key_value, is_int_key, make_private_name_id};
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
use crate::ic_helper::{self, ic_get_hit, ic_set_hit};
|
||||
@@ -46,7 +46,7 @@ impl Vm {
|
||||
|
||||
fn primitive_property_get(&mut self, val: JsValue, prop_name_si: u32) -> Result<Option<JsValue>, String> {
|
||||
if val.is_string() {
|
||||
let length_si = self.kernel_core.perm_interner().intern("length").0;
|
||||
let length_si = self.length_si;
|
||||
if prop_name_si == length_si {
|
||||
// SAFETY: val 是字符串值。utf16_len 构造时缓存,O(1) 读取。
|
||||
let len = unsafe { (*val.as_string_ptr()).utf16_len() };
|
||||
@@ -522,15 +522,33 @@ impl Vm {
|
||||
fn dispatch_get_prop_dynamic(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> {
|
||||
vm_trace!("GET_PROP_DYNAMIC rd={} a={} b={}", rd, a, b);
|
||||
let prop_name_si = self.property_key_si(self.regs[a])?;
|
||||
if let Some(value) = self.primitive_property_get(self.regs[rd], prop_name_si)? {
|
||||
let receiver = self.regs[rd];
|
||||
// 数组元素 fast path:整数键 + 真数组 + 无元素 meta(无 hole/accessor/描述符)
|
||||
// + 界内 → 直读元素区,免 primitive_property_get / checked_object_ptr /
|
||||
// ordinary_get 调用链。判定条件与 ordinary_get_inner 数组分支严格等价。
|
||||
// 非对象 receiver(如 `"abc"[0]`)须先 is_object 快检再 checked_object_ptr,
|
||||
// 否则该检查会误抛 TypeError(原路径对原始值走 primitive_property_get 不抛)。
|
||||
if is_int_key(prop_name_si) && receiver.is_object() {
|
||||
if let Some(obj_ptr) = self.checked_object_ptr(receiver, "GET_PROP_DYNAMIC on non-object")? {
|
||||
let obj = unsafe { &*obj_ptr };
|
||||
if obj.is_array() && obj.array_elements_meta_vec().is_none() {
|
||||
let idx = int_key_value(prop_name_si);
|
||||
if idx < obj.array_prop_count {
|
||||
self.regs[b] = obj.get_prop_at(idx);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(value) = self.primitive_property_get(receiver, prop_name_si)? {
|
||||
self.regs[b] = value;
|
||||
return Ok(());
|
||||
}
|
||||
let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "GET_PROP_DYNAMIC on non-object")? else {
|
||||
let Some(obj_ptr) = self.checked_object_ptr(receiver, "GET_PROP_DYNAMIC on non-object")? else {
|
||||
return Ok(());
|
||||
};
|
||||
let obj = unsafe { &*obj_ptr };
|
||||
let val = self.ordinary_get_with_target(obj, prop_name_si, self.regs[rd], b as u8)?;
|
||||
let val = self.ordinary_get_with_target(obj, prop_name_si, receiver, b as u8)?;
|
||||
if self.accessor_frame_target_reg.take().is_none() {
|
||||
self.regs[b] = val;
|
||||
}
|
||||
@@ -543,7 +561,28 @@ impl Vm {
|
||||
return Ok(());
|
||||
};
|
||||
let prop_name_si = self.property_key_si(self.regs[a])?;
|
||||
if self.kernel_core.perm_interner().lookup(prop_name_si) == Some("__proto__") {
|
||||
// 数组元素 fast path:整数键 + 真数组 + 无 meta + 界内 → 直写元素区,
|
||||
// 免 __proto__ 查询 / ordinary_set_dispatch / get_own_property_slot 调用链。
|
||||
// 与 ordinary_set_inner 数组分支等价(界内写不触发 length 扩展、无 shadow
|
||||
// 同步)。越界写必须走原路径(CreateDataProperty + length 扩展),绝不可
|
||||
// 直写——set_prop_storage 对越界下标会落到 hash_props 属性区。
|
||||
if is_int_key(prop_name_si) {
|
||||
let obj = unsafe { &*obj_ptr };
|
||||
if obj.is_array() && obj.array_elements_meta_vec().is_none() {
|
||||
let idx = int_key_value(prop_name_si);
|
||||
if idx < obj.array_prop_count {
|
||||
// promote 必须保留:obj 是 session 根、写入值可能是 epoch 对象,
|
||||
// 不 promote 下轮 GC 会悬垂。
|
||||
let value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[b]);
|
||||
let obj = unsafe { &mut *obj_ptr };
|
||||
obj.set_prop_storage(idx as usize, value);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
// 整数键恒不可能等于 "__proto__"(字符串 intern id 远小于整数键区间),
|
||||
// 短路免去每次 set 的 RwLock 查询。
|
||||
if !is_int_key(prop_name_si) && self.kernel_core.perm_interner().lookup(prop_name_si) == Some("__proto__") {
|
||||
let proto_value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[b]);
|
||||
if self.is_object_prototype(obj_ptr) && !proto_value.is_null() {
|
||||
self.raise_type_error("Object.prototype.__proto__ is immutable")?;
|
||||
|
||||
@@ -308,6 +308,10 @@ pub struct Vm {
|
||||
pub(crate) frames: SmallVec<[CallFrame; 16]>,
|
||||
pub(crate) kernel_core: Arc<KernelCore>,
|
||||
pub(crate) session: KernelSession,
|
||||
/// `"length"` 属性键的 intern id 缓存:进程内稳定(PermInterner append-only、
|
||||
/// KernelCore 不重建),属性 get/set 热路径免每次 intern(hash64 + DashMap +
|
||||
/// RwLock 读锁)。
|
||||
pub(crate) length_si: u32,
|
||||
pub epoch: Epoch,
|
||||
pub object_prototype: P<JsObject>,
|
||||
/// `%GeneratorPrototype%`:生成器实例的原型(next/return/throw 方法挂此)。
|
||||
@@ -1003,7 +1007,7 @@ impl Vm {
|
||||
|
||||
pub(crate) fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option<JsValue> {
|
||||
vm_trace!("resolve_property: shape_id={} prop_name_si={}", obj.shape_id(), prop_name_si);
|
||||
let length_si = self.kernel_core.perm_interner().intern("length").0;
|
||||
let length_si = self.length_si;
|
||||
if obj.is_array() && prop_name_si == length_si {
|
||||
return Some(obj.logical_len_value());
|
||||
}
|
||||
@@ -1042,7 +1046,7 @@ impl Vm {
|
||||
}
|
||||
|
||||
pub(crate) fn get_own_property_slot(&self, obj: &JsObject, prop_name_si: u32) -> Option<u32> {
|
||||
let length_si = self.kernel_core.perm_interner().intern("length").0;
|
||||
let length_si = self.length_si;
|
||||
if obj.is_array() && prop_name_si == length_si {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ impl Vm {
|
||||
&mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, target_reg: Option<u8>,
|
||||
) -> Result<JsValue, String> {
|
||||
vm_trace!("ordinary_get_inner: shape={} prop_si={}", obj.shape_id(), prop_name_si);
|
||||
let length_si = self.kernel_core.perm_interner().intern("length").0;
|
||||
let length_si = self.length_si;
|
||||
let mut current = Some(obj);
|
||||
let mut depth = 0usize;
|
||||
while let Some(obj) = current {
|
||||
@@ -186,7 +186,7 @@ impl Vm {
|
||||
// 数组 length 赋值:ArraySetLength 语义(ToUint32 + 调整元素区)。
|
||||
// 旧行为会把 length 存成影子命名属性,导致 `arr.length = N` 后
|
||||
// prop_count/迭代/内置方法看到的长度不一致。
|
||||
let length_si = self.kernel_core.perm_interner().intern("length").0;
|
||||
let length_si = self.length_si;
|
||||
if obj.is_array() && prop_name_si == length_si {
|
||||
let pc_before = self.pc;
|
||||
let number_len = self.coerce_number_bounded(val)?;
|
||||
|
||||
@@ -21,6 +21,9 @@ impl Vm {
|
||||
let core = KernelCore::new(KernelConfig::minimal());
|
||||
let mut session = KernelSession::new(&core);
|
||||
bindings::init_kernel_builtins(&core, &mut session);
|
||||
// 在 builtin 绑定之后取 id:绑定过程已 intern "length",此处命中缓存得到
|
||||
// 非 0 的稳定 id(保持枚举层"id 0 哨兵"的既有约定,见 walk_own_keys)。
|
||||
let length_si = core.perm_interner().intern("length").0;
|
||||
let obj_proto = P::clone(&session.builtin_world().object_proto);
|
||||
let mut vm = Self {
|
||||
regs: [JsValue::undefined(); 256],
|
||||
@@ -31,6 +34,7 @@ impl Vm {
|
||||
frames: smallvec::SmallVec::new(),
|
||||
kernel_core: core,
|
||||
session,
|
||||
length_si,
|
||||
epoch: Epoch::new(),
|
||||
object_prototype: obj_proto,
|
||||
generator_proto: P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
|
||||
@@ -115,6 +119,9 @@ impl Vm {
|
||||
pub fn with_kernel_core(core: Arc<KernelCore>) -> Self {
|
||||
let mut session = KernelSession::new(&core);
|
||||
bindings::init_kernel_builtins(&core, &mut session);
|
||||
// 在 builtin 绑定之后取 id:绑定过程已 intern "length",此处命中缓存得到
|
||||
// 非 0 的稳定 id(保持枚举层"id 0 哨兵"的既有约定,见 walk_own_keys)。
|
||||
let length_si = core.perm_interner().intern("length").0;
|
||||
let obj_proto = P::clone(&session.builtin_world().object_proto);
|
||||
let mut vm = Self {
|
||||
regs: [JsValue::undefined(); 256],
|
||||
@@ -125,6 +132,7 @@ impl Vm {
|
||||
frames: smallvec::SmallVec::new(),
|
||||
kernel_core: core,
|
||||
session,
|
||||
length_si,
|
||||
epoch: Epoch::new(),
|
||||
object_prototype: obj_proto,
|
||||
generator_proto: P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
|
||||
@@ -637,10 +645,7 @@ mod tests {
|
||||
// wrapper 是跨重置存活对象,其原型链必须仍能解析出 call/apply/bind:
|
||||
// 任何一条路径失效都说明 wrapper 原型指向了已释放的旧 Function 原型。
|
||||
assert_eq!(run_source(&mut vm, "Array.prototype.push.call([1], 2)"), JsValue::int(2));
|
||||
assert_eq!(
|
||||
run_source(&mut vm, "Array.prototype.push.apply([], [1, 2, 3])"),
|
||||
JsValue::int(3)
|
||||
);
|
||||
assert_eq!(run_source(&mut vm, "Array.prototype.push.apply([], [1, 2, 3])"), JsValue::int(3));
|
||||
let replaced = run_source(&mut vm, "String.prototype.replace.call('a', 'a', 'b')");
|
||||
assert_eq!(vm.lookup_str(replaced).as_deref(), Some("b"));
|
||||
let has = run_source(&mut vm, "Object.prototype.hasOwnProperty.call({x: 1}, 'x')");
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
//! 03.2 数组元素访问 dispatch 内联 fast path 语义测试:验证 fast path 命中
|
||||
//! (密集数组 int 键界内直读写)与各回退场景(越界、hole、accessor、非数组、
|
||||
//! 非对象 receiver)与 ordinary 属性路径行为一致。
|
||||
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_types::value::JsValue;
|
||||
use oxide_vm::vm::Vm;
|
||||
|
||||
fn eval(source: &str) -> Result<(Vm, JsValue), String> {
|
||||
let allocator = oxide_parser::Allocator::default();
|
||||
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
|
||||
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
|
||||
let mut vm = Vm::new();
|
||||
let result = vm.run(&module)?;
|
||||
Ok((vm, result))
|
||||
}
|
||||
|
||||
fn eval_str(source: &str) -> Result<String, String> {
|
||||
let (_vm, v) = eval(source)?;
|
||||
if v.is_string() {
|
||||
Ok(oxide_runtime_api::to_string(v))
|
||||
} else {
|
||||
Ok(format!("{v}"))
|
||||
}
|
||||
}
|
||||
|
||||
// 密集数组整数键界内读:命中 fast path 直读元素区。
|
||||
#[test]
|
||||
fn dense_index_read_hits_fast_path() {
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a[1]").unwrap(), "2");
|
||||
assert_eq!(eval_str("var a=[10,20,30]; a[0]+a[2]").unwrap(), "40");
|
||||
}
|
||||
|
||||
// 密集数组整数键界内写:命中 fast path 直写元素区,读回一致。
|
||||
#[test]
|
||||
fn dense_index_write_hits_fast_path() {
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a[1]=9; a[1]").unwrap(), "9");
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a[0]=5; a[0]").unwrap(), "5");
|
||||
}
|
||||
|
||||
// 字符串规范数字键与整数键等价(`a["1"]` == `a[1]`,同一 fast path 键)。
|
||||
#[test]
|
||||
fn string_canonical_index_key_equivalent() {
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a[\"1\"]").unwrap(), "2");
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a[\"1\"]=7; a[1]").unwrap(), "7");
|
||||
}
|
||||
|
||||
// 整值 double 键(`a[1.0]`)与整数键等价。
|
||||
#[test]
|
||||
fn integral_double_key_equivalent() {
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a[1.0]").unwrap(), "2");
|
||||
}
|
||||
|
||||
// 越界读不命中 fast path,落原型链(undefined / 原型上元素)。
|
||||
#[test]
|
||||
fn out_of_bounds_read_falls_to_proto_chain() {
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a[5]").unwrap(), "undefined");
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a.__proto__={5:\"x\"}; a[5]").unwrap(), "x");
|
||||
assert_eq!(eval_str("Array.prototype[9]=\"proto9\"; var a=[1,2,3]; a[9]").unwrap(), "proto9");
|
||||
}
|
||||
|
||||
// 越界写不命中 fast path,走 CreateDataProperty + length 扩展。
|
||||
#[test]
|
||||
fn out_of_bounds_write_extends_length() {
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a[5]=50; a.length").unwrap(), "6");
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a[5]=50; a[5]").unwrap(), "50");
|
||||
}
|
||||
|
||||
// delete 后的 hole:元素 meta 非空,不命中 fast path,读落原型链。
|
||||
#[test]
|
||||
fn deleted_hole_read_falls_to_proto() {
|
||||
assert_eq!(eval_str("var a=[1,2,3]; delete a[1]; a[1]").unwrap(), "undefined");
|
||||
}
|
||||
|
||||
// defineProperty accessor:元素 meta 非空,不命中 fast path,getter/setter 仍触发。
|
||||
#[test]
|
||||
fn accessor_getter_and_setter_still_trigger() {
|
||||
assert_eq!(
|
||||
eval_str("var a=[1,2,3]; Object.defineProperty(a,1,{get:function(){return 99;}}); a[1]").unwrap(),
|
||||
"99"
|
||||
);
|
||||
assert_eq!(
|
||||
eval_str(
|
||||
"var a=[1,2,3]; Object.defineProperty(a,1,{set:function(v){this._s=v;},get:function(){return this._s;}}); a[1]=7; a[1]"
|
||||
)
|
||||
.unwrap(),
|
||||
"7"
|
||||
);
|
||||
}
|
||||
|
||||
// 非写属性(writable:false)写入:fast path 不命中,走完整协议抛 TypeError。
|
||||
#[test]
|
||||
fn non_writable_index_write_throws() {
|
||||
let r = eval_str(
|
||||
"var a=[1,2,3]; Object.defineProperty(a,0,{writable:false,value:1}); try { a[0]=9; 'no' } catch(e) { e instanceof TypeError ? 'throws' : 'wrong' }"
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(r, "throws", "expected TypeError, got {r:?}");
|
||||
}
|
||||
|
||||
// 非规范数字串(前导零)不映射整数键,走字符串键慢路径(不命中 fast path)。
|
||||
#[test]
|
||||
fn non_canonical_index_string_uses_slow_path() {
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a[\"05\"]").unwrap(), "undefined");
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a[\"05\"]=9; a.length").unwrap(), "3");
|
||||
}
|
||||
|
||||
// 负索引/小数键不进入整数键区间,走字符串键慢路径。
|
||||
#[test]
|
||||
fn negative_and_fractional_keys_use_slow_path() {
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a[-1]").unwrap(), "undefined");
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a[1.5]").unwrap(), "undefined");
|
||||
}
|
||||
|
||||
// 普通对象(非数组)整数键不受 fast path 影响,走普通属性路径。
|
||||
#[test]
|
||||
fn plain_object_int_key_unaffected() {
|
||||
assert_eq!(eval_str("var o={0:\"x\"}; o[0]").unwrap(), "x");
|
||||
assert_eq!(eval_str("var o={}; o[0]=\"y\"; o[0]").unwrap(), "y");
|
||||
}
|
||||
|
||||
// 非对象 receiver(`"abc"[0]`):is_object 快检先行,不 raise、返回 undefined。
|
||||
#[test]
|
||||
fn primitive_string_index_does_not_raise() {
|
||||
assert_eq!(eval_str("\"abc\"[0]").unwrap(), "undefined");
|
||||
assert_eq!(eval_str("var a=\"abc\"; a[0]").unwrap(), "undefined");
|
||||
}
|
||||
|
||||
// push 构造的数组 fast path 读写与 length 一致。
|
||||
#[test]
|
||||
fn push_built_array_fast_path_consistent() {
|
||||
assert_eq!(eval_str("var a=[]; a.push(1); a.push(2); a.length").unwrap(), "2");
|
||||
assert_eq!(eval_str("var a=[]; a.push(1); a.push(2); a[0]+a[1]").unwrap(), "3");
|
||||
}
|
||||
|
||||
// TypedArray 整数索引不命中数组 fast path(is_array 为假),走 TA 分支。
|
||||
#[test]
|
||||
fn typed_array_int_index_unaffected() {
|
||||
assert_eq!(eval_str("var t=new Int32Array([5,6,7]); t[1]").unwrap(), "6");
|
||||
}
|
||||
|
||||
// fast path 写后 length 读一致(shadow length 同步不受影响)。
|
||||
#[test]
|
||||
fn fast_path_write_keeps_length_in_sync() {
|
||||
assert_eq!(eval_str("var a=[1,2,3]; a[0]=7; a.length").unwrap(), "3");
|
||||
}
|
||||
|
||||
// 空数组 / 稀疏空洞(字面量 hole,无 meta)读 undefined,与慢路径一致。
|
||||
#[test]
|
||||
fn sparse_literal_hole_reads_undefined() {
|
||||
assert_eq!(eval_str("var a=[10,,30]; a[1]").unwrap(), "undefined");
|
||||
}
|
||||
|
||||
// 越界写后读取原型链元素(fast path 不越界直写)。
|
||||
#[test]
|
||||
fn oob_write_then_proto_read() {
|
||||
assert_eq!(eval_str("Array.prototype[9]=\"proto9\"; var a=[1,2,3]; a[9]=90; a[9]").unwrap(), "90");
|
||||
}
|
||||
Reference in New Issue
Block a user