Repository navigation
perf(codegen,gc): declare a class's typed-shape layout at allocation, not after its constructor (#7510 item 1, #7512) - #7532
Merged
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#7510 item 1 (construction), which absorbed the residual half of #7512. Item 2 (death) shipped in #7525; item 3 (slot notes) is untouched.
The defect
js_gc_init_typed_shape_layoutwas emitted after the constructor call, so no raw-f64 class-field store inside a constructor could pass itsGC_OBJ_TYPED_LAYOUT_INTACTguard — neither the inlineand i16 %r, 4096test norclass_field_fast_contract'slayout_typed_raw_f64_slot_for_user. Every one fell back tojs_put_value_set.The one-line reorder does not work, and that is the whole design problem:
init_typed_shape_layoutvalidates that each raw-f64 slot already holds a plain double, and a fresh slot holdsTAG_UNDEFINED, whose0x7FFCtag sits insidelayout_raw_f64_bits' reject range. An early call downgrades every instance it touches. (test_validating_install_refuses_a_freshly_allocated_instancepins this as executable documentation — if that predicate ever acceptsundefined, the test fails and the whole split becomes removable.)The change
A second runtime entry point,
js_gc_declare_typed_shape_layout, skips slot validation. That moves the burden of proof to codegen, wheretyped_shape::class_layout_declarable_at_allocationdischarges it with two conditions:ctor_prologue_param_assigned_fields, non-empty only for a class with no heritage, no field initializers or computed keys, no decorators, plain parameters, and no setter shadowing an assigned field. ALocalGetof a plain parameter cannot throw, allocate, or observethis, so nothing can read a raw-f64 slot between the declaration and its first write. Every, not some — one field assigned later would still be exposed.POINTER_FREE— byte-identical to whatlayout_init_pointer_freealready sets on every fresh instance. The only delta emitted is the intact bit and the shape-shared descriptor install; the collector's view at birth is unchanged. Classes with pointer fields would installSIDE_MASKover the allocator's fill: sound on the pre-filling allocation path, but it would rest on that pre-fill, so they are out of scope.One predicate drives both emitters — the declaration is emitted iff the post-constructor install is suppressed — so they cannot drift into double-installing or into leaving an instance with no descriptor.
Nothing rests on the values actually being numbers. A constructor that stores a string into a
number-declared field is rejected by the store guard (is_plain_number_bits, and the inline path's finite-exponent test), falls back to the boxed setter, and downgrades the descriptor throughlayout_note_slot— the same path any post-install contradiction has always taken.Measurements
Pinned quiet host (load 2.0, wall == user), interleaved, best-of-7:
push_cls—new Node(v, w)×20Mpush_cls_read— same, plus reading both fields backThe collector result is strictly better, not merely equal.
push_cls: promotion 210,488 → 64 bytes, copied 0.0042 → 0.0036 GB, peak RSS 29.6 → 24.1 MB, cycles unchanged at 105. Those are the numbers the equivalent object literal already had (churn: 0.0036 GB, 64 B, 24.2 MB) — #7512's anomaly closed on the memory axis as well as the time one.churn,churn_alloc,push_num,tree,deeplist,churn_readcompile to byte-identical generated objects across the two arms, so their ±2% is host noise by construction. (Compared as object bytes — the object cache key hashes codegen env and compiler identity per #6394, so filenames always differ and cannot answer this.)Object literals do not qualify — and the reason is a finding
HIR rewrites a closed-shape literal to
new __AnonShape_<hash>(…), whose synthesized constructor is a qualifying prologue. But the minted class's field types come out asAny, notNumber.Anyis pointer-bearing, so the gate refuses — and there was no raw-f64 store path to unlock anyway.The corollary is worth its own ticket:
{v: number, w: number}is currently declared to the collector as two pointer slots. The literal path's remaining construction cost is a type-propagation gap, not an ordering one, and it is a different lever from this ticket's.Testing
cargo test -p perry-runtime— 1767 pass, 0 fail.cargo test -p perry-codegen— failure set identical toorigin/main(22 pre-existing; the twoext_registryentries that appeared once did not reproduce in four further full-suite runs — a shared-registry test-order flake).perry-codegen/tests/typed_shape_declared_at_allocation.rs(9 tests): the declaration dominates the constructor call, replaces the post-constructor install, and carries a raw-f64 mask with a null pointer mask — plus six negatives (non-prologue number field, pointer field, untyped field, all-boolean class, no constructor).perry-runtime/src/gc/tests/layout_trace/declared_at_allocation.rs(5 tests): the validating install still refuses a fresh instance, the declaring one accepts it, both still reject a slot-count mismatch and overlapping masks, and a contradicting store both evicts the descriptor and leaves the object conservatively scanned so its string child is still traced.number-declared field, collected hard — all 20,000 readable,typeofstring, matchingnodeexactly, under default and underPERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_VERIFY_EVACUATION=1. A read-before-write probe confirms the gate refuses both shapes that would expose a pre-write slot (the declare symbol is not even linked) and matchesnode.test-files/programs: byte-identical output across both arms, and identical again under GC zeal + from-space protection.cargo fmt --all -- --checkclean;scripts/check_file_size.shclean —new.rswas 19 lines under the 2000-line cap and this change would have pushed it to 2042, hence thelower_call/typed_shape_initsplit.What's left on #7510
Item 3 (
layout_note_slotcompiling away on a store that still matches the canonical mask, ~6% ofchurn_alloc) is untouched, and the ticket's headline acceptance (≥1.5× onchurn_alloc,gc::layoutunder 8%) still is not met —churn_allocis the literal path, which as above needs the type-propagation fix, not this one. #7510 stays open.Summary by CodeRabbit
Performance
Reliability
Documentation