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) }