From 5ecb428320dd7abe69427ea56bea1b2961c15429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=89=B9=E6=AF=8F=E7=BE=8A=E9=98=9F?= Date: Mon, 29 Jun 2026 14:04:50 +0800 Subject: [PATCH] =?UTF-8?q?perf(vm):=20=E5=AD=97=E7=AC=A6=E4=B8=B2?= =?UTF-8?q?=E6=8B=BC=E6=8E=A5=E7=94=A8=20push=5Fto=5Fstring=20=E6=B6=88?= =?UTF-8?q?=E5=8E=BB=E4=B8=AD=E9=97=B4=E5=88=86=E9=85=8D,=20GC=E9=98=88?= =?UTF-8?q?=E5=80=BC=208M=E2=86=9232M?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - oxide_runtime_api 新增 push_to_string: 直接将 JsValue 写入 String buffer - dispatch_add / dispatch_compound_add 复用 Vm::string_buf, 每次concat从4次分配降到1次 - vm_support 新增 new_string_owned 接受 String 转移所有权 - kernel session_gc_threshold 8MB→32MB 减少频繁 GC - build.sh: make clean 清跨平台 .obj 残留 (cherry picked from commit ad7b044a5a24b23819d783c5419d3e33f936dfc5) --- benchmark/build.sh | 1 + crates/oxide_kernel/src/kernel.rs | 6 +-- crates/oxide_runtime_api/src/lib.rs | 48 ++++++++++++++++++++++ crates/oxide_vm/src/dispatch/arithmetic.rs | 20 +++++---- crates/oxide_vm/src/vm.rs | 2 + crates/oxide_vm/src/vm_support.rs | 12 +++++- 6 files changed, 76 insertions(+), 13 deletions(-) diff --git a/benchmark/build.sh b/benchmark/build.sh index 211ea0e..2708272 100644 --- a/benchmark/build.sh +++ b/benchmark/build.sh @@ -76,6 +76,7 @@ build_quickjs() { return 0 fi info "编译 QuickJS..." + make clean 2>&1 | tail -1 # 清理跨平台残留 .obj,避免 PE/ELF 混用 if make -j"$(nproc 2>/dev/null || echo 4)" all 2>&1; then info "QuickJS 编译成功" else diff --git a/crates/oxide_kernel/src/kernel.rs b/crates/oxide_kernel/src/kernel.rs index 207fdb2..98fde46 100644 --- a/crates/oxide_kernel/src/kernel.rs +++ b/crates/oxide_kernel/src/kernel.rs @@ -39,7 +39,7 @@ impl KernelConfig { max_dead_strings: Some(10_000), max_steps: None, max_call_depth: 1024, - session_gc_threshold: 8_388_608, + session_gc_threshold: 33_554_432, max_cached_modules: 512, log_levels: [Level::Off; SUBSYSTEM_COUNT], warmup_builtin_shapes: true, @@ -55,7 +55,7 @@ impl KernelConfig { max_dead_strings: Some(10_000), max_steps: None, max_call_depth: 1024, - session_gc_threshold: 8_388_608, + session_gc_threshold: 33_554_432, max_cached_modules: 512, log_levels: [Level::Off; SUBSYSTEM_COUNT], warmup_builtin_shapes: true, @@ -71,7 +71,7 @@ impl KernelConfig { max_dead_strings: Some(5_000), max_steps: None, max_call_depth: 1024, - session_gc_threshold: 8_388_608, + session_gc_threshold: 33_554_432, max_cached_modules: 512, log_levels: [Level::Off; SUBSYSTEM_COUNT], warmup_builtin_shapes: true, diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs index e7a40fe..2bb3b30 100644 --- a/crates/oxide_runtime_api/src/lib.rs +++ b/crates/oxide_runtime_api/src/lib.rs @@ -193,6 +193,54 @@ pub fn to_int32(val: JsValue) -> i32 { } } +/// Append the string representation of `val` to `buf` without allocating +/// intermediate temporary Strings. Used in hot-path string concatenation. +pub fn push_to_string(val: JsValue, buf: &mut String) { + if val.is_int() { + use std::fmt::Write; + let _ = write!(buf, "{}", val.as_int()); + return; + } + if val.is_double() { + let d = val.as_double(); + if d.is_nan() { + buf.push_str("NaN"); + return; + } + if d.is_infinite() { + buf.push_str(if d.is_sign_positive() { "Infinity" } else { "-Infinity" }); + return; + } + if d.is_finite() && d.fract() == 0.0 { + use std::fmt::Write; + let _ = write!(buf, "{}", d as i64); + return; + } + let mut ryubuf = ryu::Buffer::new(); + buf.push_str(ryubuf.format(d)); + return; + } + if val.is_bool() { + buf.push_str(if val.as_bool() { "true" } else { "false" }); + return; + } + if val.is_null() { + buf.push_str("null"); + return; + } + if val.is_undefined() { + buf.push_str("undefined"); + return; + } + if val.is_string() { + unsafe { buf.push_str(string_data(val)) }; + return; + } + if val.is_object() { + buf.push_str("[object]"); + } +} + pub fn to_string(val: JsValue) -> String { if val.is_int() { return val.as_int().to_string(); diff --git a/crates/oxide_vm/src/dispatch/arithmetic.rs b/crates/oxide_vm/src/dispatch/arithmetic.rs index 69b5959..9f24940 100644 --- a/crates/oxide_vm/src/dispatch/arithmetic.rs +++ b/crates/oxide_vm/src/dispatch/arithmetic.rs @@ -10,10 +10,12 @@ impl Vm { let lhs = self.coerce_primitive_bounded(self.regs[a], false)?; let rhs = self.coerce_primitive_bounded(self.regs[b], false)?; if lhs.is_string() || rhs.is_string() { - let ls = coercion::to_string(lhs); - let rs = coercion::to_string(rhs); - let concat = format!("{ls}{rs}"); - self.regs[rd] = self.new_string(&concat); + let mut buf = std::mem::take(&mut self.string_buf); + buf.clear(); + coercion::push_to_string(lhs, &mut buf); + coercion::push_to_string(rhs, &mut buf); + self.regs[rd] = self.new_string(&buf); + self.string_buf = buf; } else { let ln = coercion::to_number(lhs); let rn = coercion::to_number(rhs); @@ -43,10 +45,12 @@ impl Vm { let lhs = self.coerce_primitive_bounded(self.regs[rd], false)?; let rhs = self.coerce_primitive_bounded(self.regs[a], false)?; if lhs.is_string() || rhs.is_string() { - let ls = coercion::to_string(lhs); - let rs = coercion::to_string(rhs); - let concat = format!("{ls}{rs}"); - self.regs[rd] = self.new_string(&concat); + let mut buf = std::mem::take(&mut self.string_buf); + buf.clear(); + coercion::push_to_string(lhs, &mut buf); + coercion::push_to_string(rhs, &mut buf); + self.regs[rd] = self.new_string(&buf); + self.string_buf = buf; } else { let ln = coercion::to_number(lhs); let rn = coercion::to_number(rhs); diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index a1abb8c..7c6ad12 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -189,6 +189,8 @@ pub struct Vm { pub(crate) iters: IterState, /// Grouped inline-cache and instruction counters. pub(crate) profiling: ProfilingState, + /// Reusable string buffer for concatenation to avoid allocation per `+` op. + pub(crate) string_buf: String, } impl Vm { diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 39f9529..d9e483f 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -69,6 +69,7 @@ impl Vm { ic_misses: std::cell::Cell::new(0), instruction_count: 0, }, + string_buf: String::new(), }; vm_info!("Vm created"); vm @@ -127,6 +128,7 @@ impl Vm { ic_misses: std::cell::Cell::new(0), instruction_count: 0, }, + string_buf: String::new(), }; vm_info!("Vm created (pool)"); vm @@ -220,9 +222,15 @@ impl Vm { } pub fn new_string(&mut self, s: &str) -> JsValue { - let ptr = Box::into_raw(Box::new(JsString::new(s.to_string()))); + self.new_string_owned(s.to_string()) + } + + /// Same as `new_string` but takes `String` by move, avoiding a clone. + pub fn new_string_owned(&mut self, s: String) -> JsValue { + let len = s.len(); + let ptr = Box::into_raw(Box::new(JsString::new(s))); self.gc_state.session_string_ptrs.push(ptr); - self.gc_state.session_bytes_allocated += std::mem::size_of::() + s.len(); + self.gc_state.session_bytes_allocated += std::mem::size_of::() + len; JsValue::string(ptr) }