You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Constructing a class instance is 63% slower than constructing the equivalent object
literal, on an otherwise identical workload. new Node(v, w) with two declared number
fields is 28.5× Node; the {v, w} literal is 17.4×.
That is backwards. A fixed-shape class with declared primitive fields is the most
statically-known construction form Perry has — known field count, known types, known
layout, a real constructor to attach a shape to. It should be the fastest path, not the
slowest.
This is filed separately from the two profile-driven sibling tickets because it looks
bug-shaped rather than optimisation-shaped, and may be a cheap bisect rather than a
project.
Evidence
Quiet M1 mini (perry-macos.local, load ~1.4), best-of-3 wall clock. Both programs do the
same thing — 20,000 rounds × 1000 objects pushed into a fresh local array:
variant
Perry
node
scriptc
ratio vs node
churn_alloc.ts — keep.push({ v: base + j, w: j })
2.44 s
0.14 s
—
17.4×
push_cls.ts — keep.push(new Node(base + j, j))
3.99 s
0.14 s
0.46 s
28.5×
push_num.ts — keep.push(base + j) (no object)
0.30 s
0.11 s
—
2.7×
Node treats the two forms as equivalent (0.14 s both). scriptc does push_cls in 0.46 s — 8.7× faster than Perry on the class form, while Perry beats scriptc comfortably on
pure compute (fib(40): 0.39 s vs 0.78 s). So this is specific to class construction.
Both are named for class-field stores yet appear on the object-literal benchmark;
profiling push_cls directly should show whether they grow. Other candidates worth ruling
in or out early:
js_gc_init_typed_shape_layout per new (gc/layout.rs:962) — emitted by lower_call/new.rs on every construction. Does the class path emit more layout work than
the literal path, which can use the shape cache?
js_ctor_return_override (0.9% on the literal profile) — constructor-return semantics
should be statically resolvable for a plain class.
Whether the constructor body's this.v = v stores go through the generic field-set path
instead of a direct slot store. perf(codegen): elide provably-dead per-store bookkeeping on class-field stores #7486 elided "provably-dead per-store bookkeeping on
class-field stores" — check whether its precondition actually holds here, because the
measurement says something is still being paid.
Whether the class instance misses js_object_alloc_class_inline_keys' fast path
(object/alloc.rs:242) that the literal hits.
Task
Profile push_cls symbolicated and diff the group shares against churn_alloc. The
delta is 1.55 s of a 3.99 s run — it should be plainly visible, not subtle.
Identify why the class path pays more, and fix it so that class construction is at
worst equal to the equivalent object literal.
push_cls.ts is no slower thanchurn_alloc.ts on a quiet host (today 3.99 s vs
2.44 s).
push_cls.ts ratio vs Node drops from 28.5× to at most the literal form's ratio.
A regression test pins class construction against object-literal construction so the
ordering cannot silently invert again.
No GC-behaviour drift: PERRY_GC_TRACE=1 on churn still ~105 cycles / ~0.004 GB copied
with positive reclamation every cycle.
cargo test workspace sweep green (exclude cross-host UI crates on macOS).
Traps
Do not benchmark on the dev Mac while builds run — load 15–140 from other agents
makes wall clock meaningless. Use ssh perry@perry-macos.local (M1, 8 cores, idles
~1.5); Perry binaries are static arm64, so ship them over rather than building there.
Fallback on a loaded host: best-of-N user CPU, within ~5% of the quiet host's wall
clock.
PERRY_DEBUG_SYMBOLS=1 at compile time, or sample output is all ???.
Rebuild runtime and stdlib (the .a comes from the -static wrapper crates); PERRY_NO_AUTO_OPTIMIZE=1 on ad-hoc compiles; rm -rf node_modules/.cache/perry after
switching compilers; never CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 for measured builds.
PERRY_WRITE_BARRIERS=0 makes these benchmarks slower (it switches the collector out
of evacuating mode) — it is not a way to isolate barrier cost.
Context
Sibling tickets from the same investigation: #7510 (gc::layout construction-path
cost, 33.6% of self time, under umbrella #5094) and #7511 (write barriers on
provably-non-pointer stores, 16.1%). Related: #7486 (class-field store elision), #7469 (_tlv_get_addr),
repsel 4a/4b (#6915, #6919), #6759 (object model).
Summary
Constructing a class instance is 63% slower than constructing the equivalent object
literal, on an otherwise identical workload.
new Node(v, w)with two declarednumberfields is 28.5× Node; the
{v, w}literal is 17.4×.That is backwards. A fixed-shape class with declared primitive fields is the most
statically-known construction form Perry has — known field count, known types, known
layout, a real constructor to attach a shape to. It should be the fastest path, not the
slowest.
This is filed separately from the two profile-driven sibling tickets because it looks
bug-shaped rather than optimisation-shaped, and may be a cheap bisect rather than a
project.
Evidence
Quiet M1 mini (
perry-macos.local, load ~1.4), best-of-3 wall clock. Both programs do thesame thing — 20,000 rounds × 1000 objects pushed into a fresh local array:
churn_alloc.ts—keep.push({ v: base + j, w: j })push_cls.ts—keep.push(new Node(base + j, j))push_num.ts—keep.push(base + j)(no object)Node treats the two forms as equivalent (0.14 s both). scriptc does
push_clsin 0.46 s —8.7× faster than Perry on the class form, while Perry beats scriptc comfortably on
pure compute (fib(40): 0.39 s vs 0.78 s). So this is specific to class construction.
push_cls.ts:Programs are in
gc-handoff/bench/(push_cls.ts,churn_alloc.ts,push_num.ts).Where to look
The symbolicated profile of the literal form already shows a large typed-feedback
component that the class path plausibly pays more of:
js_typed_feedback_class_field_set_guard(typed_feedback/guards.rs:542) — 4.9%typed_feedback::guards::class_field_*— 4.3%Both are named for class-field stores yet appear on the object-literal benchmark;
profiling
push_clsdirectly should show whether they grow. Other candidates worth rulingin or out early:
js_gc_init_typed_shape_layoutpernew(gc/layout.rs:962) — emitted bylower_call/new.rson every construction. Does the class path emit more layout work thanthe literal path, which can use the shape cache?
js_ctor_return_override(0.9% on the literal profile) — constructor-return semanticsshould be statically resolvable for a plain class.
this.v = vstores go through the generic field-set pathinstead of a direct slot store. perf(codegen): elide provably-dead per-store bookkeeping on class-field stores #7486 elided "provably-dead per-store bookkeeping on
class-field stores" — check whether its precondition actually holds here, because the
measurement says something is still being paid.
js_object_alloc_class_inline_keys' fast path(
object/alloc.rs:242) that the literal hits.Task
push_clssymbolicated and diff the group shares againstchurn_alloc. Thedelta is 1.55 s of a 3.99 s run — it should be plainly visible, not subtle.
worst equal to the equivalent object literal.
of perf(gc): layout side tables are 34% of object construction — the construction/death half of #5094 (allocation is 7.7%) #7510 / perf(gc): write barriers cost 16% on an all-numeric store workload — elide on provably-non-pointer stores #7511 rather than fixing it twice — but record the finding, because
"classes are slower than literals" is the symptom most likely to be noticed by users.
Acceptance criteria
push_cls.tsis no slower thanchurn_alloc.tson a quiet host (today 3.99 s vs2.44 s).
push_cls.tsratio vs Node drops from 28.5× to at most the literal form's ratio.ordering cannot silently invert again.
PERRY_GC_TRACE=1on churn still ~105 cycles / ~0.004 GB copiedwith positive reclamation every cycle.
cargo testworkspace sweep green (exclude cross-host UI crates on macOS).Traps
makes wall clock meaningless. Use
ssh perry@perry-macos.local(M1, 8 cores, idles~1.5); Perry binaries are static arm64, so ship them over rather than building there.
Fallback on a loaded host: best-of-N user CPU, within ~5% of the quiet host's wall
clock.
PERRY_DEBUG_SYMBOLS=1at compile time, orsampleoutput is all???..acomes from the-staticwrapper crates);PERRY_NO_AUTO_OPTIMIZE=1on ad-hoc compiles;rm -rf node_modules/.cache/perryafterswitching compilers; never
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16for measured builds.PERRY_WRITE_BARRIERS=0makes these benchmarks slower (it switches the collector outof evacuating mode) — it is not a way to isolate barrier cost.
Context
Sibling tickets from the same investigation: #7510 (
gc::layoutconstruction-pathcost, 33.6% of self time, under umbrella #5094) and #7511 (write barriers on
provably-non-pointer stores, 16.1%). Related: #7486 (class-field store elision), #7469 (
_tlv_get_addr),repsel 4a/4b (#6915, #6919), #6759 (object model).