From 6c6d4320a62010344c7bf1b772256d8cae979909 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 14 Aug 2026 21:00:43 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20replace=20=E5=87=BD=E6=95=B0=20replacer?= =?UTF-8?q?=20=E5=9B=9E=E8=B0=83=E5=85=8D=E6=AF=8F=E5=8C=B9=E9=85=8D?= =?UTF-8?q?=E6=95=B4=E4=B8=B2=E5=A4=8D=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回调第 4 参(原字符串)由每匹配复制整串改为调用方预构一次: 原始字符串 receiver 直接复用原值零拷贝,对象 receiver 提升为单次会话串。 覆盖 String.prototype.replace/replaceAll 与 RegExp.prototype[Symbol.replace] 的函数 replacer 路径。补回调第 4 参断言测试与 fn-replacer 压测。 --- crates/oxide_builtins/src/regexp.rs | 6 +++- crates/oxide_builtins/src/string.rs | 28 ++++++++++++------- .../oxide_vm/tests/builtins_string_tests.rs | 22 +++++++++++++++ tests/stress/string_replace_fn.js | 16 +++++++++++ 4 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 tests/stress/string_replace_fn.js diff --git a/crates/oxide_builtins/src/regexp.rs b/crates/oxide_builtins/src/regexp.rs index 96391a79..29998e4b 100644 --- a/crates/oxide_builtins/src/regexp.rs +++ b/crates/oxide_builtins/src/regexp.rs @@ -384,7 +384,11 @@ pub fn regexp_symbol_replace(vm: &mut H, args: &[u8]) -> NativeResult if replacer_val.is_object() { let o = unsafe { &*replacer_val.as_js_object_ptr() }; if o.is_function() { - crate::string::regex_replace_fn(vm, regex, &haystack, replacer_val, is_global) + // 回调第 4 参(原字符串)预构一次:原始字符串参数直接复用零拷贝, + // 对象参数复用已转换的 haystack 建单个会话串(原每匹配整串复制)。 + let text_val = vm.reg(if args.len() > 1 { args[1] } else { args[0] }); + let text_arg = if text_val.is_string() { text_val } else { vm.new_string(&haystack) }; + crate::string::regex_replace_fn(vm, regex, &haystack, replacer_val, is_global, text_arg) } else { let replacement = oxide_runtime_api::to_string(replacer_val); NativeResult::Ok(vm.new_string(&crate::string::regex_replace_manual( diff --git a/crates/oxide_builtins/src/string.rs b/crates/oxide_builtins/src/string.rs index 9f2bff37..eceae781 100644 --- a/crates/oxide_builtins/src/string.rs +++ b/crates/oxide_builtins/src/string.rs @@ -306,8 +306,9 @@ fn call_replacer(vm: &mut H, replacer: JsValue, cb_args: &[JsValue]) } /// 函数 replacer 的回调参数:匹配串、各捕获组(未匹配为 undefined)、position、原字符串。 -/// position 按字符索引(非字节偏移)。 -fn replacer_cb_args(vm: &mut H, text: &str, m: ®ress::Match) -> Vec { +/// position 按字符索引(非字节偏移)。原字符串参数由调用方预构传入(`text_arg`), +/// 避免每匹配复制整个源串——字符串不可变,同一 `JsValue` 可安全复用。 +fn replacer_cb_args(vm: &mut H, text: &str, m: ®ress::Match, text_arg: JsValue) -> Vec { let range = m.range(); let mut cb_args: Vec = Vec::with_capacity(m.captures.len() + 2); cb_args.push(vm.new_string(&text[range.start..range.end])); @@ -318,14 +319,15 @@ fn replacer_cb_args(vm: &mut H, text: &str, m: ®ress::Match) -> Ve } } cb_args.push(JsValue::int(text[..range.start].chars().count() as i32)); - cb_args.push(vm.new_string(text)); + cb_args.push(text_arg); cb_args } /// 正则模式 + 函数 replacer:global 全替换否则替换首个,逐匹配调用回调, -/// 返回值 ToString 作为替换文本(不展开 `$` 引用)。 +/// 返回值 ToString 作为替换文本(不展开 `$` 引用)。`text_arg` 为回调第 4 参 +/// (原字符串),调用方预构一次,回调期按值复用。 pub(crate) fn regex_replace_fn( - vm: &mut H, regex: ®ress::Regex, text: &str, replacer: JsValue, global: bool, + vm: &mut H, regex: ®ress::Regex, text: &str, replacer: JsValue, global: bool, text_arg: JsValue, ) -> NativeResult { let matches: Vec = if global { regex.find_iter(text).collect() @@ -337,7 +339,7 @@ pub(crate) fn regex_replace_fn( for m in matches { let range = m.range(); out.push_str(&text[last_end..range.start]); - let cb_args = replacer_cb_args(vm, text, &m); + let cb_args = replacer_cb_args(vm, text, &m, text_arg); let repl = try_string!(call_replacer(vm, replacer, &cb_args)); out.push_str(&repl); last_end = range.end; @@ -348,7 +350,10 @@ pub(crate) fn regex_replace_fn( /// 字符串模式 + 函数 replacer:all 全替换否则替换首个。回调参数 /// `(match, position, string)`(无捕获组)。空模式在每个字符边界匹配一次。 -fn string_replace_fn(vm: &mut H, text: &str, pattern: &str, replacer: JsValue, all: bool) -> NativeResult { +/// `text_arg` 为回调第 4 参(原字符串),调用方预构一次。 +fn string_replace_fn( + vm: &mut H, text: &str, pattern: &str, replacer: JsValue, all: bool, text_arg: JsValue, +) -> NativeResult { let search_length = pattern.len(); let mut positions: Vec = Vec::new(); if search_length == 0 { @@ -378,7 +383,7 @@ fn string_replace_fn(vm: &mut H, text: &str, pattern: &str, replacer: let cb_args = [ vm.new_string(&text[p..p + search_length]), JsValue::int(text[..p].chars().count() as i32), - vm.new_string(text), + text_arg, ]; let repl = try_string!(call_replacer(vm, replacer, &cb_args)); out.push_str(&repl); @@ -1079,16 +1084,19 @@ fn string_replace_impl(vm: &mut H, args: &[u8], all: bool) -> NativeR // 会 E0499,receiver 必须 owned 本地 String(跨回调的硬约束)。 if let Some(replacer_val) = replacer_val { let s = try_string!(this_string(vm, args)).into_owned(); + // 回调第 4 参(原字符串)预构一次:原始字符串 receiver 直接复用 + // this_val 零拷贝,对象 receiver 提升为单次会话串(原每匹配整串复制)。 + let text_arg = if this_val.is_string() { this_val } else { vm.new_string(&s) }; if has_native_re { let re_ptr = pattern_val.as_js_object_ptr(); let re = unsafe { &*re_ptr }; let fn_ptr = re.native_fn().unwrap(); // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; - return regex_replace_fn(vm, regex, &s, replacer_val, is_global); + return regex_replace_fn(vm, regex, &s, replacer_val, is_global, text_arg); } let pattern = as_string(vm, pattern_val).into_owned(); - return string_replace_fn(vm, &s, &pattern, replacer_val, all); + return string_replace_fn(vm, &s, &pattern, replacer_val, all, text_arg); } let replacement_is_string = args.len() <= 2 || replacement_val.is_string(); diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs index a3dc53f4..3569a955 100644 --- a/crates/oxide_vm/tests/builtins_string_tests.rs +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -333,6 +333,28 @@ fn string_replace_function_replacer_string_pattern() { assert_eq!(to_str(&vm, s), "a1c"); } +#[test] +fn string_replace_function_replacer_string_arg() { + // 回调第 4 参为原字符串:原始 receiver 直接传值(=== 原串), + // 对象 receiver 传 ToString 内容;字符串/正则 pattern、replace/replaceAll 全覆盖。 + let mut vm = Vm::new(); + let s = eval(&mut vm, "'abc'.replace('b', function(m, o, str){ return str })").unwrap(); + assert_eq!(to_str(&vm, s), "aabcc"); + let s = eval(&mut vm, "'abc'.replace(/b/, function(m, o, str){ return str })").unwrap(); + assert_eq!(to_str(&vm, s), "aabcc"); + let s = eval(&mut vm, "'abc'.replaceAll('b', function(m, o, str){ return str })").unwrap(); + assert_eq!(to_str(&vm, s), "aabcc"); + // 第 4 参与原始字符串按内容恒等(=== 对字符串按值比较)。 + let s = eval(&mut vm, "var str='abc'; str.replace('b', function(m, o, str2){ return str2 === str })").unwrap(); + assert_eq!(to_str(&vm, s), "atruec"); + // boxed receiver 走对象 ToString 路径,第 4 参为解箱后内容。 + let s = eval(&mut vm, "new String('abc').replace('b', function(m, o, str){ return str })").unwrap(); + assert_eq!(to_str(&vm, s), "aabcc"); + // RegExp.prototype[Symbol.replace] 直调路径(非 String.prototype.replace 快路径)。 + let s = eval(&mut vm, "/b/[Symbol.replace]('abc', function(m, o, str){ return str })").unwrap(); + assert_eq!(to_str(&vm, s), "aabcc"); +} + #[test] fn string_replace_missing_replacement_is_undefined_string() { // 缺失 replaceValue 按 ToString(undefined)="undefined" 替换(非空串)。 diff --git a/tests/stress/string_replace_fn.js b/tests/stress/string_replace_fn.js new file mode 100644 index 00000000..a8f5f104 --- /dev/null +++ b/tests/stress/string_replace_fn.js @@ -0,0 +1,16 @@ +// 函数 replacer 场景:global 正则多匹配 + 长源串,回调第 4 参为原字符串。 +// 每匹配整串复制的消除在此路径可测(m×n 字节拷贝 → 0)。 +var ITERATIONS = 2000; + +var s = ""; +for (var i = 0; i < 50; i++) { + s += "ab" + i + "c"; +} +var sum = 0; +for (var i = 0; i < ITERATIONS; i++) { + var r = s.replace(/b/g, function (m) { + return "X"; + }); + sum += r.length; +} +sum