Skip to content

perf: new Klass(v,w) is 63% slower than the equivalent object literal (28.5x vs 17.4x Node) #7512

Description

@proggeramlug

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 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.tskeep.push({ v: base + j, w: j }) 2.44 s 0.14 s 17.4×
push_cls.tskeep.push(new Node(base + j, j)) 3.99 s 0.14 s 0.46 s 28.5×
push_num.tskeep.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.

push_cls.ts:

class Node {
  v: number;
  w: number;
  constructor(v: number, w: number) { this.v = v; this.w = w; }
}
function chunk(base: number): number {
  const keep: Node[] = [];
  for (let j = 0; j < 1000; j++) keep.push(new Node(base + j, j));
  return keep.length;
}
function main(): void {
  let total = 0;
  for (let c = 0; c < 20000; c++) total += chunk(c * 1000);
  console.log(total);
}
main();

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_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

  1. 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.
  2. Identify why the class path pays more, and fix it so that class construction is at
    worst equal to the equivalent object literal.
  3. If the cause turns out to be the layout or barrier subsystems, close this as a duplicate
    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

  1. push_cls.ts is no slower than churn_alloc.ts on a quiet host (today 3.99 s vs
    2.44 s).
  2. push_cls.ts ratio vs Node drops from 28.5× to at most the literal form's ratio.
  3. A regression test pins class construction against object-literal construction so the
    ordering cannot silently invert again.
  4. No GC-behaviour drift: PERRY_GC_TRACE=1 on churn still ~105 cycles / ~0.004 GB copied
    with positive reclamation every cycle.
  5. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions