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
Browse filesBrowse the repository at this point in the historyBrowse files
Ralph Küpper
committed
perf(codegen): stamp a pointer-free shape's typed layout into the allocation header
Four allocation benchmarks sat within a 6% band at 2.50-2.65x node, which is
the signature of one shared per-allocation cost rather than four problems.
Profiling the band (200M-allocation variants, two samples each, agreeing
within 1.5pp) found it: `js_gc_declare_typed_shape_layout` was 30% of
`churn_alloc` and `push_cls`, and it spent that re-deriving per OBJECT a fact
that is a property of the SHAPE. #7510's memo had already reduced the map
round-trip to a direct-mapped probe; what remained was the probe itself, a
type-table lookup, a field-count compare, and the cross-crate call.
For a shape whose pointer mask is statically EMPTY the answer is a constant:
`GC_LAYOUT_POINTER_FREE | GC_OBJ_TYPED_LAYOUT_INTACT`. The inline-bump `new`
path already writes a packed `GcHeader` constant that carries the state half,
so the intact bit is folded into the same store and the call disappears. What
survives is the one half that depends on the recycled ADDRESS rather than the
shape - clearing a previous tenant's per-object record - now a one-argument
`js_gc_forget_object_layout` behind a `PERRY_PER_OBJECT_LAYOUTS_ANY` test whose
`0` state proves every thread's per-object tables empty.
Two smaller levers in the same band:
* `js_ctor_return_override` was called per construction to answer a question
that is `undefined` for every constructor without an explicit `return` -
8% of `churn_alloc`. `JSValue::is_undefined` is `bits == TAG_UNDEFINED`, so
one 64-bit compare decides it inline and the runtime call stays on the cold
arm, where derived-constructor TypeErrors and object returns still need it.
* A `new` in a function the hot-loop-callee pre-pass admitted is a `new` in a
loop one frame out, so it takes the inline bump too. `cycles.ts`'s
`makeCycle` is the shape: 5 statements, hence `alwaysinline` and hence never
`inlinehint`, so the existing gate read the one flag it could not have.
Measured on the quiet M1 mini, best-of-5, outputs byte-identical to node with
exit 0 verified for all 27 programs:
| bench | before | after |
|---|--:|--:|
| churn | 0.4217 | 0.2900 |
| churn_alloc | 0.3720 | 0.2409 |
| push_cls | 0.3665 | 0.2368 |
`churn_alloc` goes 18.6 -> 12.0 ns per allocation (node is 7.1 on the same
shape). `gc-handoff/bench/alloc_declare_{pf,ptr}.ts` isolate it: identical
programs differing only in whether the second field's declared type makes the
pointer mask non-empty.
Soundness: the collector's view is bit-identical. `heap_payload_slot_selection`
skips a `GC_LAYOUT_POINTER_FREE` payload without consulting any map, and the
pre-existing path also reached `POINTER_FREE` for an empty pointer mask. A
later pointer store still downgrades - with no descriptor to classify against,
`layout_note_slot` falls through to its generic pointer-mask branch, which
mints a per-object mask and flips the state to `SIDE_MASK`, needing no
descriptor at all. A pointer-BEARING shape keeps the full runtime declare,
because its `SIDE_MASK` state means the tracer reads a mask and that call is
what installs it.
0 commit comments