Skip to content

Commit 0cfc307

Browse files
Haofeiclaude
andcommitted
perf(vm): copy-on-write struct field writes
SetField previously cloned the entire field map and allocated a new Rc<VmStruct> on every `obj.field = ...` (value semantics via rebuild) — the dominant cost in mut-binding field-write loops. Now write_field_value_owned takes the struct out of its register and mutates in place via Rc::get_mut when the Rc is uniquely owned (no other observer → observationally identical to rebuilding); it copy-on-writes (clone + rebuild) only when the Rc is shared. struct_field_rw benchmark: ~141ms → ~86ms. Gated by a new 5-way differential test (backends_agree_on_struct_field_writes) covering the in-place loop + read-parameter sharing; full suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 3e1b0d3 commit 0cfc307

3 files changed

Lines changed: 91 additions & 21 deletions

File tree

src/reg_vm/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6000,7 +6000,11 @@ impl RegVm {
60006000
} => {
60016001
let obj_reg = base + *obj;
60026002
let new_value = self.reg(base + *value).clone();
6003-
let updated = write_field_value(self.reg(obj_reg), name, new_value)?;
6003+
// Take the struct out so its `Rc` count reflects only other live
6004+
// holders; `write_field_value_owned` then mutates in place when
6005+
// uniquely owned, or copy-on-writes when shared.
6006+
let current = self.take_reg(obj_reg);
6007+
let updated = write_field_value_owned(current, name, new_value)?;
60046008
self.set_reg(obj_reg, updated);
60056009
self.set_reg(base + *dst, VmValue::Unit);
60066010
}

src/reg_vm/value_convert.rs

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -333,33 +333,31 @@ pub(super) fn read_field_ref(value: &VmValue, field: &str) -> Result<VmValue, Ev
333333
/// Return a copy of the struct/variant `value` with `field` set to `new_value`.
334334
/// Structs are value types, so this rebuilds the struct. A `Managed` wrapper is
335335
/// updated in place (its interior is shared and mutable by design).
336-
pub(super) fn write_field_value(
337-
value: &VmValue,
336+
/// Set `field`, mutating the struct in place when the value is the sole owner of
337+
/// its `Rc` (copy-on-write). A uniquely-owned struct has no other observer, so
338+
/// in-place mutation is observationally identical to rebuilding — but avoids
339+
/// cloning the entire field map and allocating a new `Rc` on every `obj.field =
340+
/// ...` (the dominant cost in `mut`-binding field-write loops). Falls back to
341+
/// clone + rebuild when the `Rc` is shared.
342+
pub(super) fn write_field_value_owned(
343+
value: VmValue,
338344
field: &str,
339345
new_value: VmValue,
340346
) -> Result<VmValue, EvalError> {
341347
match value {
342-
VmValue::Struct(data) | VmValue::Variant(data) => {
343-
if !data.fields.contains_key(field) {
344-
return Err(EvalError::Runtime(format!(
345-
"reg VM struct value is missing field `{field}`."
346-
)));
347-
}
348-
let mut fields = data.fields.clone();
349-
fields.insert(field.to_string(), new_value);
350-
let updated = Rc::new(VmStruct {
351-
name: Rc::clone(&data.name),
352-
fields,
353-
});
354-
Ok(match value {
355-
VmValue::Variant(_) => VmValue::Variant(updated),
356-
_ => VmValue::Struct(updated),
357-
})
348+
VmValue::Struct(mut data) => {
349+
write_struct_field_in_place(&mut data, field, new_value)?;
350+
Ok(VmValue::Struct(data))
351+
}
352+
VmValue::Variant(mut data) => {
353+
write_struct_field_in_place(&mut data, field, new_value)?;
354+
Ok(VmValue::Variant(data))
358355
}
359356
VmValue::Managed(inner) => {
360-
let updated = write_field_value(&inner.borrow(), field, new_value)?;
357+
let current = inner.borrow().clone();
358+
let updated = write_field_value_owned(current, field, new_value)?;
361359
*inner.borrow_mut() = updated;
362-
Ok(VmValue::Managed(Rc::clone(inner)))
360+
Ok(VmValue::Managed(inner))
363361
}
364362
other => Err(EvalError::Runtime(format!(
365363
"reg VM expected Struct for field `{field}`, got `{}`.",
@@ -368,6 +366,37 @@ pub(super) fn write_field_value(
368366
}
369367
}
370368

369+
fn write_struct_field_in_place(
370+
data: &mut Rc<VmStruct>,
371+
field: &str,
372+
new_value: VmValue,
373+
) -> Result<(), EvalError> {
374+
if let Some(unique) = Rc::get_mut(data) {
375+
return match unique.fields.get_mut(field) {
376+
Some(slot) => {
377+
*slot = new_value;
378+
Ok(())
379+
}
380+
None => Err(EvalError::Runtime(format!(
381+
"reg VM struct value is missing field `{field}`."
382+
))),
383+
};
384+
}
385+
// Shared `Rc`: copy-on-write to preserve value semantics for the other holders.
386+
if !data.fields.contains_key(field) {
387+
return Err(EvalError::Runtime(format!(
388+
"reg VM struct value is missing field `{field}`."
389+
)));
390+
}
391+
let mut fields = data.fields.clone();
392+
fields.insert(field.to_string(), new_value);
393+
*data = Rc::new(VmStruct {
394+
name: Rc::clone(&data.name),
395+
fields,
396+
});
397+
Ok(())
398+
}
399+
371400
pub(super) fn unmanage_vm_value(value: VmValue) -> VmValue {
372401
match value {
373402
VmValue::Managed(value) => unmanage_vm_value(value.borrow().clone()),

tests/backend_differential.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,43 @@ fn main() -> Unit {
382382
common::differential::assert_backends_agree("jit-collections.rss", source, &[]);
383383
}
384384

385+
/// A mut-heavy struct field-write loop plus mut/read parameter passing — gates the
386+
/// copy-on-write `SetField` (mutate in place when the struct `Rc` is uniquely
387+
/// owned, clone when shared). The results must match the compiled backend, which
388+
/// uses owned Rust structs.
389+
#[test]
390+
fn backends_agree_on_struct_field_writes() {
391+
let source = "\
392+
struct Box {
393+
v: Int,
394+
w: Int
395+
}
396+
397+
fn total(b: read Box) -> Int {
398+
return b.v + b.w
399+
}
400+
401+
fn main() -> Unit {
402+
let mut a = Box(v: 1, w: 2)
403+
let mut i = 0
404+
while i < 20 {
405+
a.v = a.v + a.w
406+
a.w = a.w + i + a.v
407+
// `total` borrows `a` (read) mid-loop — exercises the shared-Rc path
408+
// before the next in-place write.
409+
let snapshot = total(b: read a)
410+
a.v = a.v + snapshot
411+
i = i + 1
412+
}
413+
Log.write(message: read String.from_int(value: a.v))
414+
Log.write(message: read String.from_int(value: a.w))
415+
Log.write(message: read String.from_int(value: total(b: read a)))
416+
return Unit
417+
}
418+
";
419+
common::differential::assert_backends_agree("struct-field-writes.rss", source, &[]);
420+
}
421+
385422
/// Deque / Set / SortedSet / SortedMap mutations with out-of-order inserts,
386423
/// duplicates, and removes, then sorted/ordered dumps. This gates the collection
387424
/// backing/representation optimizations: the VM (`Vec`-backed) and the compiled

0 commit comments

Comments
 (0)