From bfbab9431afe7dcbf2241a52bb86a95e9aa15a84 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 5 Oct 2026 07:36:12 +0000 Subject: [PATCH] =?UTF-8?q?perf:=20String()=20=E6=9C=89=E9=99=90=20double?= =?UTF-8?q?=20=E8=BD=AC=E4=B8=B2=E5=8A=A0=2016=20=E6=A7=BD=20last-value=20?= =?UTF-8?q?=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 36 +++++------ Cargo.toml | 2 +- crates/oxide_builtins/src/string/basic.rs | 5 +- .../tests/string_constructor_tests.rs | 59 +++++++++++++++++++ crates/oxide_runtime_api/src/host.rs | 8 +++ crates/oxide_vm/src/vm/gc_hooks.rs | 6 ++ crates/oxide_vm/src/vm/mod.rs | 6 ++ crates/oxide_vm/src/vm/vmhost.rs | 3 + crates/oxide_vm/src/vm_support.rs | 41 +++++++++++++ 9 files changed, 145 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db0a0eb3..c3d9b133 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1095,7 +1095,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.8.1" +version = "0.8.2" dependencies = [ "chrono", "fast-float", @@ -1120,7 +1120,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.8.1" +version = "0.8.2" dependencies = [ "num-bigint", "oxide_log", @@ -1128,7 +1128,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.8.1" +version = "0.8.2" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1139,7 +1139,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.8.1" +version = "0.8.2" dependencies = [ "ansi_term", "bumpalo", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.8.1" +version = "0.8.2" dependencies = [ "lru", "oxide_bytecode", @@ -1168,7 +1168,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.8.1" +version = "0.8.2" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1187,7 +1187,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.8.1" +version = "0.8.2" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1201,7 +1201,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.8.1" +version = "0.8.2" dependencies = [ "num-bigint", "oxc_allocator", @@ -1216,7 +1216,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.8.1" +version = "0.8.2" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1225,7 +1225,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.8.1" +version = "0.8.2" dependencies = [ "bumpalo", "dashmap", @@ -1240,7 +1240,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.8.1" +version = "0.8.2" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1252,7 +1252,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.8.1" +version = "0.8.2" dependencies = [ "tracing", "tracing-appender", @@ -1261,7 +1261,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.8.1" +version = "0.8.2" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.8.1" +version = "0.8.2" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1292,7 +1292,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.8.1" +version = "0.8.2" dependencies = [ "num-bigint", "num-traits", @@ -1304,7 +1304,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.8.1" +version = "0.8.2" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.8.1" +version = "0.8.2" dependencies = [ "bumpalo", "hashbrown 0.14.5", @@ -1333,7 +1333,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.8.1" +version = "0.8.2" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index e4a5ec14..86fd5536 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ ] [workspace.package] -version = "0.8.1" +version = "0.8.2" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/string/basic.rs b/crates/oxide_builtins/src/string/basic.rs index a8db2e80..8714f50b 100644 --- a/crates/oxide_builtins/src/string/basic.rs +++ b/crates/oxide_builtins/src/string/basic.rs @@ -164,8 +164,9 @@ pub fn string_constructor(vm: &mut H, args: &[u8]) -> NativeResult { } else if d.is_infinite() { JsValue::string(oxide_kernel::string_forge::const_string_ptr(if d > 0.0 { 5 } else { 6 })) } else { - // 有限 double 走原数值转串链,行为与转换链一致。 - vm.new_string_owned(oxide_runtime_api::js_number_to_string(d)) + // 有限 double 走十六槽 last-value 缓存:命中复用已登记串, + // miss 走原数值转串链,行为与转换链一致。 + vm.number_to_string_cached(d) } } else { // BigInt/对象参数走完整转换链(对象可经 valueOf/toString 抛错); diff --git a/crates/oxide_builtins/tests/string_constructor_tests.rs b/crates/oxide_builtins/tests/string_constructor_tests.rs index 77ca146a..9467fdac 100644 --- a/crates/oxide_builtins/tests/string_constructor_tests.rs +++ b/crates/oxide_builtins/tests/string_constructor_tests.rs @@ -90,3 +90,62 @@ fn string_constructor_fallback_paths() { let (_vm, len) = eval("new String(\"ab\").length").unwrap(); assert_eq!(len.as_int(), 2); } + +// 位模式低 4 位互异的十六个有限 double(槽位互不冲突的测试前提)。 +const CACHE_TEST_VALUES: [f64; 16] = [ + 0.119, 0.103, 0.087, 0.071, 0.063, 0.207, 0.175, 0.143, 0.015, 0.007, 0.003, 0.005, 0.001, 0.205, 0.173, 0.141, +]; + +#[test] +fn number_to_string_cache_slot_correctness() { + // 十六个槽位互异的值两轮各调一次:命中须键全等才返回, + // 钉住槽位碰撞不得错返。 + let mut vm = Vm::new(); + let mut seen = [false; 16]; + for &d in &CACHE_TEST_VALUES { + let slot = (d.to_bits() as usize) & 15; + assert!(!seen[slot], "槽位冲突:{d}"); + seen[slot] = true; + } + for _round in 0..2 { + for &d in &CACHE_TEST_VALUES { + let v = vm.number_to_string_cached(d); + assert!(v.is_string()); + let expected = oxide_runtime_api::js_number_to_string(d); + assert_eq!(unsafe { &*v.as_string_ptr() }.as_str(), expected.as_str(), "值 {d} 应与原转换链一致"); + } + } +} + +#[test] +fn number_to_string_cache_survives_session_gc() { + // 填满十六槽后强制完整 session 收集:缓存串已登记为 GC 根, + // 收集后读回仍为原内容(漏根登记此测必挂)。 + let mut vm = Vm::new(); + for &d in &CACHE_TEST_VALUES { + vm.number_to_string_cached(d); + } + vm.collect_session_gc(); + for &d in &CACHE_TEST_VALUES { + let v = vm.number_to_string_cached(d); + assert!(v.is_string()); + let expected = oxide_runtime_api::js_number_to_string(d); + assert_eq!(unsafe { &*v.as_string_ptr() }.as_str(), expected.as_str(), "收集后值 {d} 应可读回"); + } + // 2.5 挤占槽 0 后写新串:新串同样须存活后续收集。 + let v = vm.number_to_string_cached(2.5); + vm.collect_session_gc(); + let v2 = vm.number_to_string_cached(2.5); + assert_eq!(unsafe { &*v2.as_string_ptr() }.as_str(), "2.5"); + assert_eq!(v.as_string_ptr(), v2.as_string_ptr()); +} + +#[test] +fn number_to_string_cache_identity() { + // 同值两次调用命中同槽:返回指针相等(命中复用 session 串)。 + let mut vm = Vm::new(); + let a = vm.number_to_string_cached(2.5); + let b = vm.number_to_string_cached(2.5); + assert!(a.is_string() && b.is_string()); + assert_eq!(a.as_string_ptr(), b.as_string_ptr()); +} diff --git a/crates/oxide_runtime_api/src/host.rs b/crates/oxide_runtime_api/src/host.rs index 7d555eb0..cc5ec9e8 100644 --- a/crates/oxide_runtime_api/src/host.rs +++ b/crates/oxide_runtime_api/src/host.rs @@ -58,6 +58,14 @@ pub trait VmHost { fn new_string(&mut self, s: &str) -> JsValue; /// move 接收 `String` 创建会话字符串,避免一次整串克隆。 fn new_string_owned(&mut self, s: String) -> JsValue; + /// 有限 double 转串,带十六槽 last-value 缓存:命中返回已登记为 GC 根的 + /// session 串,miss 走原数值转串链并写槽。 + /// + /// # 注意事项 + /// - 非有限值(NaN/±Infinity)走永久常量表,不进槽。 + /// - `full_reset` 在 session 串释放前清空槽;轻量 `reset` 保留槽 + /// (session 串跨 eval 存活,缓存串仍有效)。 + fn number_to_string_cached(&mut self, d: f64) -> JsValue; /// 取 ASCII 单字符的永久字符串值:命中返回共享 perm 串(零分配、可指针 /// 短路比较),非 ASCII 返回 `None` 由调用方回落普通字符串创建。 /// diff --git a/crates/oxide_vm/src/vm/gc_hooks.rs b/crates/oxide_vm/src/vm/gc_hooks.rs index 520578a2..005c8dcf 100644 --- a/crates/oxide_vm/src/vm/gc_hooks.rs +++ b/crates/oxide_vm/src/vm/gc_hooks.rs @@ -76,6 +76,12 @@ impl Vm { for &cached in self.template_objects.values() { f(cached); } + // f64→string 缓存槽值是 session 串,是 GC 根(漏根 → 串清扫释放 → 命中悬垂)。 + for &v in &self.number_to_string_cache_vals { + if v.is_string() { + f(v); + } + } for &entry in &self.iters.for_of_iters { f(entry.iterator); f(entry.last_result); diff --git a/crates/oxide_vm/src/vm/mod.rs b/crates/oxide_vm/src/vm/mod.rs index 7143752c..0aa1bf63 100644 --- a/crates/oxide_vm/src/vm/mod.rs +++ b/crates/oxide_vm/src/vm/mod.rs @@ -183,6 +183,12 @@ pub struct Vm { /// (发射层字符串常量统一经 perm_string 物化),指针相等即键为 "length", /// 入口快判只比指针免键解析。PermInterner 追加式、指针唯一,永不失效。 pub(crate) length_perm_ptr: *const JsString, + /// f64→string 十六槽 last-value 缓存的键(double 位模式)。空槽判定用 + /// 对应值非字符串:键 0 是 +0.0 的合法位模式,不得作空标记。 + pub(crate) number_to_string_cache_keys: [u64; 16], + /// f64→string 十六槽 last-value 缓存的值(session 串,经 `for_each_value` + /// 登记为 GC 根;`full_reset` 在 session 串释放前清空)。 + pub(crate) number_to_string_cache_vals: [JsValue; 16], pub object_prototype: P, /// `%GeneratorPrototype%`:生成器实例的原型(next/return/throw 方法挂此)。 pub generator_proto: P, diff --git a/crates/oxide_vm/src/vm/vmhost.rs b/crates/oxide_vm/src/vm/vmhost.rs index 3574d5d5..c9f0ff3a 100644 --- a/crates/oxide_vm/src/vm/vmhost.rs +++ b/crates/oxide_vm/src/vm/vmhost.rs @@ -33,6 +33,9 @@ impl oxide_runtime_api::VmHost for Vm { fn new_string_owned(&mut self, s: String) -> JsValue { Vm::new_string_owned(self, s) } + fn number_to_string_cached(&mut self, d: f64) -> JsValue { + Vm::number_to_string_cached(self, d) + } fn new_bigint(&mut self, v: num_bigint::BigInt) -> JsValue { Vm::new_bigint(self, v) } diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 28a8efc0..b5f40c66 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -57,6 +57,8 @@ impl Vm { session, length_si, length_perm_ptr, + number_to_string_cache_keys: [0u64; 16], + number_to_string_cache_vals: [JsValue::undefined(); 16], object_prototype: obj_proto, generator_proto: P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), generator_function_proto: P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), @@ -198,6 +200,8 @@ impl Vm { session, length_si, length_perm_ptr, + number_to_string_cache_keys: [0u64; 16], + number_to_string_cache_vals: [JsValue::undefined(); 16], object_prototype: obj_proto, generator_proto: P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), generator_function_proto: P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), @@ -409,6 +413,10 @@ impl Vm { }), ); self.active_immutables = std::ptr::slice_from_raw_parts(std::ptr::null(), 0); + // f64→string 缓存槽指向即将释放的 session 串:须在 teardown 之前清空, + // 否则下次命中返回悬垂指针。 + self.number_to_string_cache_keys = [0u64; 16]; + self.number_to_string_cache_vals = [JsValue::undefined(); 16]; self.teardown_session_heap_data(); self.gc_state.session_bytes_allocated = 0; self.gc_state.session_bytes_peak = 0; @@ -595,6 +603,39 @@ impl Vm { self.register_session_string(ptr, len) } + /// 有限 double 转串,带十六槽 last-value 缓存:命中返回已登记的 session 串, + /// miss 走原数值转串链并写槽。 + /// + /// # 步骤 + /// 1. 非有限值(NaN/±Infinity)返回对应永久常量串,不进槽。 + /// 2. 槽位取位模式低 4 位;槽值为字符串且键全等时返回槽值。 + /// 3. miss 走原数值转串链新建 session 串,写键与值后返回。 + /// + /// # 副作用 + /// - 槽值经 `for_each_value` 登记为 GC 根,strings-only 收集不释放; + /// `full_reset` 在 session 串释放前清空槽。 + /// + /// # 注意事项 + /// - 空槽判定用值非字符串:+0.0 的位模式是 0,键不得作空标记。 + /// - direct-mapped last-value:异值撞槽即覆盖,返回前键须 64 位全等。 + pub fn number_to_string_cached(&mut self, d: f64) -> JsValue { + if d.is_nan() { + return JsValue::string(oxide_kernel::string_forge::const_string_ptr(4)); + } + if d.is_infinite() { + return JsValue::string(oxide_kernel::string_forge::const_string_ptr(if d > 0.0 { 5 } else { 6 })); + } + let bits = d.to_bits(); + let slot = (bits as usize) & 15; + if self.number_to_string_cache_vals[slot].is_string() && self.number_to_string_cache_keys[slot] == bits { + return self.number_to_string_cache_vals[slot]; + } + let v = self.new_string_owned(oxide_runtime_api::js_number_to_string(d)); + self.number_to_string_cache_keys[slot] = bits; + self.number_to_string_cache_vals[slot] = v; + v + } + /// 登记一个 session 字符串并记账(`new_string_owned` 与 `new_cons_string` /// 共用),返回字符串值。 ///