perf: IC 多态化 4 槽缓存(FIFO 滚动写回)消除多态场景 miss 重解析

每条 IC 指令扩展字 3→8(4 槽 × [shape_id|proto_depth, slot]),命中槽 0
单态快路径零额外成本,槽 1..3 懒解码遍历,miss 写回 FIFO 滚动(新条目进
槽 0、最老丢弃)。修复原型链 IC 深度解析多走一步与 prop_forge 模板不匹配
不写回两个使缓存恒失效的问题。ic_cache 基准命中率 34.8%→78.9%,
耗时 -29%;单态 prop_access 因扩展字布局膨胀有 ~20% 取指回归(后续
side-table 方案可根除)。
This commit is contained in:
2026-08-15 06:40:19 +08:00
parent 96da78e817
commit b62348eb5d
9 changed files with 388 additions and 112 deletions
@@ -0,0 +1,171 @@
//! IC 多态缓存(4 槽,FIFO 滚动写回)行为测试。
//!
//! `run()` 每次从 CodeForge 原始 bytecode 冷启动(IC 学习不跨 run 持久),
//! 因此全部断言基于**单次 run 内**的 miss 计数与迭代数的对比:多态 4 槽
//! 覆盖的 shape 只产生"每 shape 一次"的学习 miss,超出槽容量的轮换则
//! 每次访问都 miss(FIFO 逐访问滚动)。
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
/// 编译并单次运行,返回 (结果, hits, misses)。
fn run_once(source: &str) -> (String, u64, u64) {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse");
let module = Compiler::new().compile(&program).expect("compile");
let mut vm = Vm::new();
let r = vm.run(&module).expect("run").to_string();
(r, vm.ic_hit_count(), vm.ic_miss_count())
}
/// 4 种 shape 轮换访问同一 `.x` 站点:每 shape 一次学习 miss,之后全命中。
#[test]
fn poly4_shape_rotation_only_learning_misses() {
let (r, hits, misses) = run_once(
r#"var a = { x: 1 }, b = { x: 2, y: 3 }, c = { x: 4, z: 5 }, d = { x: 6, y: 7, z: 8 };
var sum = 0;
for (var i = 0; i < 10000; i++) {
var t = [a, b, c, d][i % 4];
sum += t.x;
}
sum"#,
);
assert_eq!(r, "32500", "x 值 1/2/4/6 各 2500 次累加");
assert_eq!(misses, 4, "4 shape 均被 4 槽缓存,仅首轮学习各 1 次 miss");
assert!(hits >= 9990, "后续访问全部命中(实际命中 {hits})");
}
/// 第 5 种 shape 挤掉 FIFO 最老槽:顺序轮换下每访问都 miss(缓存容量不足)。
#[test]
fn poly5_shape_rotation_evicts_oldest() {
let (r, _hits, misses) = run_once(
r#"var s = [
{ x: 1 }, { x: 2, y: 3 }, { x: 4, z: 5 }, { x: 6, y: 7, z: 8 }, { x: 9, a: 1, b: 2 }
];
var sum = 0;
for (var i = 0; i < 10000; i++) {
sum += s[i % 5].x;
}
sum"#,
);
assert_eq!(r, "44000", "x 值 1/2/4/6/9 各 2000 次累加");
assert!(
misses > 5000,
"5 shape 顺序轮换超出 4 槽容量,FIFO 逐访问滚动导致 miss 过半(实际 {misses})"
);
}
/// 单态连续访问:仅 1 次学习 miss,空槽 break 不越界。
#[test]
fn monomorphic_only_one_learning_miss() {
let (r, _hits, misses) = run_once(
r#"var a = { x: 1 };
var sum = 0;
for (var i = 0; i < 10000; i++) { sum += a.x; }
sum"#,
);
assert_eq!(r, "10000");
assert_eq!(misses, 1, "单态站点仅首轮学习 miss");
}
/// 空 IC(未学习)走慢路径并写回槽 0;首次访问即正确。
#[test]
fn cold_cache_first_access_resolves() {
let (r, hits, misses) = run_once(
r#"var a = { x: 7 };
var sum = 0;
for (var i = 0; i < 1000; i++) { sum += a.x; }
sum"#,
);
assert_eq!(r, "7000");
assert_eq!(misses, 1);
assert_eq!(hits, 999, "学习后 999 次全命中");
}
/// accessor 属性走 has_prop_meta 慢路径,不经 IC 缓存;语义正确即可。
#[test]
fn accessor_property_semantics_preserved() {
let (r, _hits, _misses) = run_once(
r#"var o = { get x() { return 1; } };
var sum = 0;
for (var i = 0; i < 100; i++) { sum += o.x; }
sum"#,
);
assert_eq!(r, "100", "getter 每次返回 1");
}
/// `__proto__` 赋值走原型设置路径,不写 IC;原型属性读语义正确。
#[test]
fn proto_assignment_not_cached_semantics_preserved() {
let (r, _hits, _misses) = run_once(
r#"var p = { m: 42 };
var o = {};
o.__proto__ = p;
var sum = 0;
for (var i = 0; i < 100; i++) { sum += o.m; }
sum"#,
);
assert_eq!(r, "4200", "__proto__ 赋值建立原型链后经 IC 原型路径读到 42");
}
/// 原型链 depth>0 多态:两个不同 proto shape 轮换,各槽独立 depth。
#[test]
fn proto_polymorphic_two_protos_learning_misses() {
let (r, _hits, misses) = run_once(
r#"var b1 = { v: 1 }, b2 = { v: 2, w: 3 };
var o1 = Object.create(b1), o2 = Object.create(b2);
var sum = 0;
for (var i = 0; i < 10000; i++) {
var o = (i % 2 === 0) ? o1 : o2;
sum += o.v;
}
sum"#,
);
assert_eq!(r, "15000");
assert_eq!(misses, 2, "两个 proto shape 各学习一次,后续 depth>0 命中");
}
/// `rerun()` 清空全部 IC 扩展字:清除后访问重新学习(对照 regression_rerun_clears_ic_cache)。
#[test]
fn rerun_clears_polymorphic_slots() {
let allocator = Allocator::default();
let program = oxide_parser::parse(
&allocator,
r#"var a = { x: 1 }, b = { x: 2, y: 3 };
var sum = 0;
for (var i = 0; i < 1000; i++) {
var t = (i % 2 === 0) ? a : b;
sum += t.x;
}
sum"#,
)
.expect("parse");
let module = Compiler::new().compile(&program).expect("compile");
let mut vm = Vm::new();
vm.run(&module).expect("run1");
let learned = vm.ic_miss_count();
// run() 每次冷启动,此处直接验证 rerun 的可重入性:清除后仍能正常执行。
assert_eq!(format!("{}", vm.rerun().expect("rerun")), "1500");
let after_rerun = vm.ic_miss_count();
assert!(
after_rerun >= learned,
"rerun 清零多态槽后重新学习(miss 不降反增,实际 {after_rerun} ≥ {learned})"
);
}
/// member 读侧(obj.x++)多 shape 轮换:read_member_prop 多态命中,
/// 写侧(set_member_prop)暂不接命中——本任务只验证格式同步不错位。
#[test]
fn member_compound_polymorphic_read_side_hits() {
let (r, _hits, misses) = run_once(
r#"var a = { x: 1 }, b = { x: 2, y: 3 };
for (var i = 0; i < 10000; i++) {
var t = (i % 2 === 0) ? a : b;
t.x += 1;
}
a.x + b.x"#,
);
assert_eq!(r, "10003", "两对象 x 各自增 5000 次(从 1/2 起)");
assert!(misses < 5000, "读侧 4 槽缓存使 miss 远低于迭代数(实际 {misses},写侧新属性路径不计入)");
}