Follow-up from #7141 (which fixed Phase 5a's arm-ordering defect: the
{method}__pshape clone had zero call sites because the typed-clone arms
shadowed the routing).
#7141 deliberately left the largest single win on the table, because taking it
needs a guard that does not exist yet. Filing it with the measurement so the
decision is on the record.
The site
benchmarks/app-patterns/kernels/batch.ts is the workload Ptr<Shape> exists
for. Its hot method call is r.rescore(1.5), inside rows.map((r) => …). The
closure parameter r has no static class, so receiver_class_name() returns
None, emit_guarded_direct_method_call is never reached, and the call lowers
to the class-id switch tower in
lower_call/property_get/dynamic_dispatch.rs (idispatch.caseN, blocks built
around :294-311).
Inside idispatch.case0 the receiver's exact dynamic class is already proven by
a runtime class_id compare, and it calls the guard-ridden public body.
What routing it would buy
Measured on the emitted IR (perry-dev, aarch64), Row::rescore:
| body |
IR lines |
js_typed_feedback_class_field_*_guard |
js_object_get_field_by_name_f64 |
…__Row__rescore (public) |
311 |
4 |
3 |
…__Row__rescore__pshape |
95 |
0 |
0 |
So one routed call site deletes 4 opaque guard calls and 3 by-name field calls
per invocation, on a 40,000-iteration loop. batch.ts totals today are 32
field guards / 47 by-name gets across the module.
Why #7141 did not do it
A class_id match is not sufficient for the clone's bare fixed-slot loads.
delete inst.field compacts the packed inline slots — js_object_delete_field
reaches the keys-scan/compaction path for any GC_TYPE_OBJECT carrying a keys
array, which is exactly what js_object_alloc_class_inline_keys installs
(crates/perry-runtime/src/object/alloc.rs:275), and there is no
"this is a class instance, tombstone instead" branch
(delete_rest.rs:291-344, field_count recomputed at :380). So on
class C { a; b; c }, delete inst.b moves c from slot 2 to slot 1 while
class_id is unchanged.
That is precisely what the keys token catches, and it is why the two
existing routing sites are safe: method_direct.fast is dominated by the
class-id + keys-token guard, and the Phase 3b site is dominated by containment.
The tower has neither.
Everything else about the layout is append-only and slot-stable: all four
allocators size the inline region once and never realloc (alloc.rs:242-294),
non-declared writes append past the declared fields and spill to
ObjectMeta::spill (field_set_by_name.rs:1785-1811, spill.rs:122-217),
js_object_set_field refuses OOB rather than growing
(field_get_set/field_ops.rs:127-136), and the shape/hidden-class machinery is
explicitly an accelerator that re-validates key bytes at the slot
(shapes.rs:12-18). delete is the sole slot relocator.
Proposed shape
Emit an inline keys check at idispatch.caseN before routing — a load of
@perry_class_keys__<mod>__<Class> plus a pointer compare, no call. The
existing inline class-field guard already does exactly this pair
(expr/class_field_inline_guard.rs:286-292 checks field_count > field_index
AND keys-array pointer equality inline), so there is precedent and a working
emitter to copy.
Net: ~3 inline instructions at the call site in exchange for 4 opaque guard
calls + 3 by-name field calls inside the body.
Needs its own soundness review for the freeze/descriptor latches the guarded
path also reads (descriptors_in_use() /
class_prototype_fast_guards_invalidated()), plus a call-site ratchet in
collectors/proven_this_routing_tests.rs and a gap test under the moving-GC
arms.
Follow-up from #7141 (which fixed Phase 5a's arm-ordering defect: the
{method}__pshapeclone had zero call sites because the typed-clone armsshadowed the routing).
#7141 deliberately left the largest single win on the table, because taking it
needs a guard that does not exist yet. Filing it with the measurement so the
decision is on the record.
The site
benchmarks/app-patterns/kernels/batch.tsis the workloadPtr<Shape>existsfor. Its hot method call is
r.rescore(1.5), insiderows.map((r) => …). Theclosure parameter
rhas no static class, soreceiver_class_name()returnsNone,emit_guarded_direct_method_callis never reached, and the call lowersto the class-id switch tower in
lower_call/property_get/dynamic_dispatch.rs(idispatch.caseN, blocks builtaround :294-311).
Inside
idispatch.case0the receiver's exact dynamic class is already proven bya runtime
class_idcompare, and it calls the guard-ridden public body.What routing it would buy
Measured on the emitted IR (perry-dev, aarch64),
Row::rescore:js_typed_feedback_class_field_*_guardjs_object_get_field_by_name_f64…__Row__rescore(public)…__Row__rescore__pshapeSo one routed call site deletes 4 opaque guard calls and 3 by-name field calls
per invocation, on a 40,000-iteration loop.
batch.tstotals today are 32field guards / 47 by-name gets across the module.
Why #7141 did not do it
A
class_idmatch is not sufficient for the clone's bare fixed-slot loads.delete inst.fieldcompacts the packed inline slots —js_object_delete_fieldreaches the keys-scan/compaction path for any
GC_TYPE_OBJECTcarrying a keysarray, which is exactly what
js_object_alloc_class_inline_keysinstalls(
crates/perry-runtime/src/object/alloc.rs:275), and there is no"this is a class instance, tombstone instead" branch
(
delete_rest.rs:291-344,field_countrecomputed at:380). So onclass C { a; b; c },delete inst.bmovescfrom slot 2 to slot 1 whileclass_idis unchanged.That is precisely what the keys token catches, and it is why the two
existing routing sites are safe:
method_direct.fastis dominated by theclass-id + keys-token guard, and the Phase 3b site is dominated by containment.
The tower has neither.
Everything else about the layout is append-only and slot-stable: all four
allocators size the inline region once and never realloc (
alloc.rs:242-294),non-declared writes append past the declared fields and spill to
ObjectMeta::spill(field_set_by_name.rs:1785-1811,spill.rs:122-217),js_object_set_fieldrefuses OOB rather than growing(
field_get_set/field_ops.rs:127-136), and the shape/hidden-class machinery isexplicitly an accelerator that re-validates key bytes at the slot
(
shapes.rs:12-18).deleteis the sole slot relocator.Proposed shape
Emit an inline keys check at
idispatch.caseNbefore routing — a load of@perry_class_keys__<mod>__<Class>plus a pointer compare, no call. Theexisting inline class-field guard already does exactly this pair
(
expr/class_field_inline_guard.rs:286-292checksfield_count > field_indexAND keys-array pointer equality inline), so there is precedent and a working
emitter to copy.
Net: ~3 inline instructions at the call site in exchange for 4 opaque guard
calls + 3 by-name field calls inside the body.
Needs its own soundness review for the freeze/descriptor latches the guarded
path also reads (
descriptors_in_use()/class_prototype_fast_guards_invalidated()), plus a call-site ratchet incollectors/proven_this_routing_tests.rsand a gap test under the moving-GCarms.