Skip to content

Commit 39fb188

Browse files
committed
Reuse string buffers across binding stores
1 parent e56aee3 commit 39fb188

5 files changed

Lines changed: 324 additions & 1 deletion

File tree

crates/qjs-runtime/src/bytecode/vm_ops.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@ impl Vm<'_> {
1818
if let Some(value) = fast_number_binary(&left, op, &right) {
1919
return Ok(value);
2020
}
21+
let (left, right) = if op == BinaryOp::Add {
22+
match (left, right) {
23+
(Value::String(left), right) => {
24+
match super::vm_string_append::primitive_append_suffix(right) {
25+
Ok(suffix) => {
26+
self.prepare_compound_string_reuse(&left);
27+
let mut result = Rc::unwrap_or_clone(left);
28+
result.push_str(&suffix);
29+
return Ok(Value::String(result.into()));
30+
}
31+
Err(right) => (Value::String(left), right),
32+
}
33+
}
34+
operands => operands,
35+
}
36+
} else {
37+
(left, right)
38+
};
2139
if let Some(value) = fast_primitive_string_binary(&left, op, &right) {
2240
return Ok(value);
2341
}

crates/qjs-runtime/src/bytecode/vm_string_append.rs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,182 @@ use crate::{GLOBAL_THIS_BINDING, RuntimeError, Value, operations};
55
use super::{ir::Op, vm::Vm, vm_bindings::is_compiler_temporary};
66

77
impl Vm<'_> {
8+
/// Drops only engine-internal mirrors before a compound string assignment.
9+
/// The following `Dup` + store bytecodes restore the binding immediately;
10+
/// any real JavaScript alias keeps the Rc shared and therefore immutable.
11+
pub(super) fn prepare_compound_string_reuse(&mut self, expected: &std::rc::Rc<String>) -> bool {
12+
if !matches!(self.bytecode.code.get(self.ip), Some(Op::Dup)) {
13+
return false;
14+
}
15+
match self.bytecode.code.get(self.ip + 1).cloned() {
16+
Some(Op::AssignLocal(slot)) => self.detach_matching_local_string(slot, expected),
17+
Some(Op::StoreGlobalStrict(name)) | Some(Op::StoreGlobalSloppy(name)) => {
18+
self.detach_matching_realm_string(&name, expected)
19+
}
20+
Some(Op::StoreLocalOrGlobalSloppy { slot, name }) => {
21+
self.detach_matching_local_string(slot, expected)
22+
|| self.detach_matching_realm_string(&name, expected)
23+
}
24+
_ => false,
25+
}
26+
}
27+
28+
fn detach_matching_local_string(
29+
&mut self,
30+
slot: usize,
31+
expected: &std::rc::Rc<String>,
32+
) -> bool {
33+
if self.direct_eval_with_stack && self.bytecode.local_is_from_env(slot) {
34+
return false;
35+
}
36+
let Some(local_meta) = self.bytecode.locals.get(slot) else {
37+
return false;
38+
};
39+
let name = local_meta.name.clone();
40+
if !local_meta.mutable
41+
|| self.env.has_module_import(&name)
42+
|| self.env.is_immutable_lexical_binding(&name)
43+
|| self.env.is_immutable_function_name(&name)
44+
|| self.local_slot_targets_non_writable_global(slot, &name)
45+
{
46+
return false;
47+
}
48+
if self.slot_is_authoritative(slot)
49+
&& let Some(Some(local)) = self.locals.get_mut(slot)
50+
&& matches!(local, Value::String(current) if std::rc::Rc::ptr_eq(current, expected))
51+
{
52+
*local = Value::Undefined;
53+
return true;
54+
}
55+
self.detach_matching_shared_string(slot, expected)
56+
}
57+
58+
/// Temporarily clears the engine's internal mirrors of one shared binding
59+
/// value so `Rc::unwrap_or_clone` can reclaim its allocation. Any actual
60+
/// JavaScript alias keeps an Rc alive and therefore still forces a copy,
61+
/// preserving string immutability. The completed assignment immediately
62+
/// restores the slot/cell/realm mirrors through the normal store path.
63+
fn detach_matching_shared_string(
64+
&mut self,
65+
slot: usize,
66+
expected: &std::rc::Rc<String>,
67+
) -> bool {
68+
if self.direct_eval_with_stack {
69+
return false;
70+
}
71+
let Some(cell) = self
72+
.local_upvalues
73+
.get(slot)
74+
.and_then(Option::as_ref)
75+
.cloned()
76+
else {
77+
return false;
78+
};
79+
let matches = cell.with_value(|value| {
80+
matches!(value, Value::String(current) if std::rc::Rc::ptr_eq(current, expected))
81+
});
82+
if !matches {
83+
return false;
84+
}
85+
86+
let name = self.bytecode.locals[slot].name.clone();
87+
let realm_cell = self.env.is_realm_binding_cell(&name, &cell);
88+
let global_this = if realm_cell {
89+
match self.realm.borrow().get(GLOBAL_THIS_BINDING).cloned() {
90+
Some(Value::Object(global_this)) => Some(global_this),
91+
_ => None,
92+
}
93+
} else {
94+
None
95+
};
96+
if let Some(property) = global_this
97+
.as_ref()
98+
.and_then(|global_this| global_this.own_property(&name))
99+
&& (property.is_accessor() || !property.writable)
100+
{
101+
return false;
102+
}
103+
104+
cell.set(Value::Undefined);
105+
if let Some(Some(local)) = self.locals.get_mut(slot)
106+
&& matches!(local, Value::String(current) if std::rc::Rc::ptr_eq(current, expected))
107+
{
108+
*local = Value::Undefined;
109+
}
110+
if realm_cell {
111+
if let Some(binding) = self.realm.borrow_mut().get_mut(&name)
112+
&& matches!(binding, Value::String(current) if std::rc::Rc::ptr_eq(current, expected))
113+
{
114+
*binding = Value::Undefined;
115+
}
116+
if let Some(global_this) = global_this
117+
&& global_this
118+
.own_property(&name)
119+
.is_some_and(|property| {
120+
matches!(property.value, Value::String(current) if std::rc::Rc::ptr_eq(&current, expected))
121+
})
122+
{
123+
global_this.set(name, Value::Undefined);
124+
}
125+
}
126+
true
127+
}
128+
129+
fn detach_matching_realm_string(&mut self, name: &str, expected: &std::rc::Rc<String>) -> bool {
130+
if self.env.has_module_import(name)
131+
|| self.env.is_immutable_lexical_binding(name)
132+
|| self.env.is_immutable_function_name(name)
133+
|| self.env.has_local_binding(name)
134+
|| self
135+
.bytecode
136+
.local_slot(name)
137+
.is_some_and(|slot| self.locals.get(slot).is_some_and(Option::is_some))
138+
{
139+
return false;
140+
}
141+
let realm_matches = self.realm.borrow().get(name).is_some_and(|value| {
142+
matches!(value, Value::String(current) if std::rc::Rc::ptr_eq(current, expected))
143+
});
144+
if !realm_matches {
145+
return false;
146+
}
147+
let global_this = match self.realm.borrow().get(GLOBAL_THIS_BINDING).cloned() {
148+
Some(Value::Object(global_this)) => Some(global_this),
149+
_ => None,
150+
};
151+
if let Some(property) = global_this
152+
.as_ref()
153+
.and_then(|global_this| global_this.own_property(name))
154+
&& (property.is_accessor() || !property.writable)
155+
{
156+
return false;
157+
}
158+
let cell = self.env.realm_binding_cell(name);
159+
if let Some(cell) = &cell
160+
&& !cell.with_value(|value| {
161+
matches!(value, Value::String(current) if std::rc::Rc::ptr_eq(current, expected))
162+
})
163+
{
164+
return false;
165+
}
166+
if let Some(cell) = cell {
167+
cell.set(Value::Undefined);
168+
}
169+
if let Some(binding) = self.realm.borrow_mut().get_mut(name) {
170+
*binding = Value::Undefined;
171+
}
172+
if let Some(global_this) = global_this
173+
&& global_this
174+
.own_property(name)
175+
.is_some_and(|property| {
176+
matches!(property.value, Value::String(current) if std::rc::Rc::ptr_eq(&current, expected))
177+
})
178+
{
179+
global_this.set(name.to_owned(), Value::Undefined);
180+
}
181+
true
182+
}
183+
8184
pub(super) fn run_string_append_op(&mut self, op: Op) -> Result<(), RuntimeError> {
9185
let result = match op {
10186
Op::AppendStringLiteralLocal { slot, value } => {
@@ -172,3 +348,16 @@ impl Vm<'_> {
172348
Ok(result)
173349
}
174350
}
351+
352+
pub(super) fn primitive_append_suffix(value: Value) -> Result<String, Value> {
353+
Ok(match value {
354+
Value::Number(number) => crate::number::number_to_js_string(number),
355+
Value::BigInt(value) => value.to_string(),
356+
Value::String(value) => std::rc::Rc::unwrap_or_clone(value),
357+
Value::Boolean(true) => "true".to_owned(),
358+
Value::Boolean(false) => "false".to_owned(),
359+
Value::Null => "null".to_owned(),
360+
Value::Undefined => "undefined".to_owned(),
361+
value => return Err(value),
362+
})
363+
}

crates/qjs-runtime/src/conversion.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub(crate) fn to_js_string_with_env(
1818
match value {
1919
Value::Number(number) => Ok(number::number_to_js_string(number)),
2020
Value::BigInt(value) => Ok(value.to_string()),
21-
Value::String(value) => Ok(value.to_string()),
21+
Value::String(value) => Ok(std::rc::Rc::unwrap_or_clone(value)),
2222
Value::Boolean(true) => Ok("true".to_owned()),
2323
Value::Boolean(false) => Ok("false".to_owned()),
2424
Value::Null => Ok("null".to_owned()),

crates/qjs-runtime/src/tests/expressions.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,50 @@ fn accumulates_string_concatenation_in_place() {
193193
eval("let s = 1; s = s + 'x'; s += 2; s;"),
194194
Ok(Value::String("1x2".to_owned().into()))
195195
);
196+
// A JavaScript alias must retain the immutable pre-assignment value even
197+
// when the binding itself can reuse its allocation.
198+
assert_eq!(
199+
eval("let s = 'a'; let old = s; s += 'b'; old + ':' + s;"),
200+
Ok(Value::String("a:ab".to_owned().into()))
201+
);
202+
// The compound-assignment reference and left value are fixed before the
203+
// RHS runs. If the RHS rebinds the target, the final store still uses the
204+
// original left value rather than appending to the intervening value.
205+
assert_eq!(
206+
eval(
207+
"let s = 'a'; function rhs() { s = 'changed'; return 'c'; } let result = s += rhs(); result + ':' + s;"
208+
),
209+
Ok(Value::String("ac:ac".to_owned().into()))
210+
);
211+
// Captured global bindings and their global-object mirrors take the same
212+
// general append path; an old alias remains unchanged.
213+
assert_eq!(
214+
eval(
215+
"var s = 'a'; var old = s; function append(v) { return s += v; } var result = append('b'); old + ':' + result + ':' + s + ':' + globalThis.s;"
216+
),
217+
Ok(Value::String("a:ab:ab:ab".to_owned().into()))
218+
);
219+
// Object operands retain ordinary ToPrimitive ordering and side effects.
220+
assert_eq!(
221+
eval(
222+
"let calls = 0; let s = 'a'; s += { toString() { calls++; return 'b'; } }; s + ':' + calls;"
223+
),
224+
Ok(Value::String("ab:1".to_owned().into()))
225+
);
226+
// Failed or ignored stores must not leave a temporarily detached binding
227+
// behind. The RHS is still evaluated before const/read-only rejection.
228+
assert_eq!(
229+
eval(
230+
"let calls = 0; function suffix() { calls++; return 'b'; } const s = 'a'; let caught = false; try { s += suffix(); } catch (error) { caught = error instanceof TypeError; } caught + ':' + calls + ':' + s;"
231+
),
232+
Ok(Value::String("true:1:a".to_owned().into()))
233+
);
234+
assert_eq!(
235+
eval(
236+
"var s = 'a'; Object.defineProperty(globalThis, 's', { writable: false }); function append(v) { return s += v; } var result = append('b'); result + ':' + s;"
237+
),
238+
Ok(Value::String("ab:a".to_owned().into()))
239+
);
196240
}
197241

198242
#[test]

tasks/T018-broad-performance.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2718,6 +2718,78 @@ and `9961943568f0540bda862cf368c7175e105bf7ac4bd811dc955c5fc49186a696`;
27182718
the next exact-SHA coverage artifact must restore the former one-gap baseline
27192719
before this optimization unit is accepted.
27202720

2721+
The exact correction commit
2722+
`e56aee3422bbba6e9f9b53ffaf0ea5d28f4241b4` closed that requirement. CI run
2723+
`29622215672` passed all jobs, and coverage run `29622349540` restored 42,671
2724+
Rust passes, one failure, zero timeouts, and one actionable gap across all
2725+
42,672 configured cases. The restored burndown/comparison SHA-256 values are
2726+
`a44f50aaf14636af13027f8228b49f53012d51f664b4379250e740d87360f1bc`
2727+
and `3ba9a1a20f4cd430f9f33471ca97652a67a12602740b26453f2e06fd63a58329`.
2728+
This is the semantic metric closure; workflow success alone was not counted.
2729+
2730+
Performance Preview run `29622215689` also completed at that exact correction
2731+
SHA. Hosted broad candidate/base was 0.987593x and candidate/QuickJS-NG was
2732+
0.355157x overall; allocation still failed B4 at 2.538944x. The other family
2733+
ratios were 0.577693x array, 0.169475x binding, 0.092127x builtin, 0.502671x
2734+
call, 0.188787x control, 0.172372x property, and 0.522510x string. Broad
2735+
raw/report SHA-256 are
2736+
`78bc381763394af7626c20453a949b7827e20e03282172f425bda6b8e2e79340`
2737+
and `e7fbfbd8264fd0b70c9eadc5708e39e0ad97b0b952826797706580c42e3155b3`.
2738+
2739+
The same exact artifact retained 5/5 JetStream, 7/14 Kraken, and 23/26
2740+
SunSpider comparable cases and again won none. Suite diagnostic ratios were
2741+
10.169012x, 6.016440x, and 10.481689x QuickJS-NG. In particular,
2742+
`string-validate-input` measured 52.310699x, which confirms that the external
2743+
string-accumulation deficit remains real despite the favorable internal broad
2744+
ratio. External raw/report SHA-256 are
2745+
`7f59bb6353e9ad02c65d5687bf2bbf765bb3f1ca4a3146f8abb449bc70e55b89`
2746+
and `5d957b9a0527bd17e5c5b0eb9bb9ae4f4bc54d216f415018763e3d71cf9e4ee7`.
2747+
2748+
The forty-eighth v2 unit follows the independent SunSpider string profile into
2749+
general primitive-string accumulation. A dynamic string `+` followed by a
2750+
binding store previously copied the whole left string even when its extra
2751+
`Rc<String>` owners were only engine-internal slot, upvalue, realm, and global
2752+
data-property mirrors. The VM now recognizes that ordinary bytecode store
2753+
shape at execution time, temporarily drops only exact-pointer internal
2754+
mirrors, and lets `Rc::unwrap_or_clone` retain the allocation and its capacity.
2755+
Any real JavaScript alias keeps an `Rc` alive and still forces copy-on-write;
2756+
RHS objects keep the full observable ToPrimitive path; immutable, accessor,
2757+
non-writable, module, `with`, and direct-eval bindings remain on their existing
2758+
paths. Moving an owned string through ToString now also avoids an unnecessary
2759+
copy. The numeric primitive fast path remains first, after the first complete
2760+
broad diagnostic caught and rejected an intermediate dispatch-order
2761+
regression. No workload name, source path, iteration count, checksum, or
2762+
expected benchmark value appears in the implementation.
2763+
2764+
On the same macOS host, exact base binary SHA-256
2765+
`2f9fafddea0f28046b4461672c8800f9aef8a3d30525544e935cd0f442974b53`
2766+
and final candidate binary SHA-256
2767+
`be9652fb29b39b0638bc9a55eba055bd477492430030852d8fc7be121eb263b4`
2768+
retained identical comparable coverage at 5/5 JetStream, 11/14 Kraken, and
2769+
23/26 SunSpider. Common-case candidate/base geometric means were 1.003227x,
2770+
0.999240x, and 1.008344x, so this is not a suite-wide speedup. The independently
2771+
sourced `string-validate-input` case nevertheless fell to 0.724812x base, and
2772+
its candidate/QuickJS-NG ratio fell from 9.318717x to 6.821436x. The candidate
2773+
still lost all comparable cases; suite ratios remained 7.849188x, 4.723542x,
2774+
and 6.182158x QuickJS-NG. Candidate external raw/report SHA-256 are
2775+
`66dd73dea64a967bded6a550ad9abbbf5b69fb7896ca797248687123eeb650cd`
2776+
and `4ab9e6475fcc855facdf6f344b26c792dc771aff3882d614bdde4d41a9b8e280`;
2777+
base raw/report SHA-256 are
2778+
`dbe7b03155fb5ec7df2b529b9af23649ac6a03ae8112c031722e659496310a7a`
2779+
and `947e157fca4ec43303ac8c71f732bc81e86031739d7808c8ef253cf0c25b2885`.
2780+
2781+
The final one-block broad diagnostic produced 75/75 eligible measurements,
2782+
600/600 passing linearity samples, and 1,462 OK samples. Candidate/base was
2783+
0.999098x overall; family ratios ranged from 0.996075x array through
2784+
1.003763x string, and the worst individual ratio was 1.029102x. Candidate/
2785+
QuickJS-NG was 0.192541x overall, but allocation still failed B4 at 1.142756x.
2786+
The strict analyzer correctly rejected the receipt-less dirty development
2787+
binaries, so this is regression evidence rather than a fixed-hardware claim.
2788+
Broad raw SHA-256 is
2789+
`47368de161f9f66180c1aed665a3b50e632818626c98a8affdcc0f778f6c3c75`.
2790+
This unit closes one profiled external mechanism without trading away the
2791+
internal families; B4 and B5 remain active.
2792+
27212793
## Historical Broad V1 Baseline
27222794

27232795
The first complete baseline was recorded on 2026-07-15 at commit

0 commit comments

Comments
 (0)