Summary
The codegen-inlined class-field shape guard (#5093) — the
class_field_inline.deref precheck that lets this.field read/write skip the
js_typed_feedback_class_field_*_guard call — never engages in ordinary
programs. Every class-field access pays a full guard call (with a thread-local
TYPED_LAYOUTS lookup) on every iteration, even for a trivial monomorphic class.
Evidence
- A/B on
benchmarks/suite/09_method_calls.ts:
PERRY_DISABLE_CLASS_FIELD_INLINE=1 → 2898 ms vs default → 2925 ms.
If the inline precheck were active by default, force-disabling it would slow
the loop down; it doesn't — so the inline path is already inactive by default.
- Instrumentation: printing on the first flip of
PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED shows it is set to 1 during runtime
startup, before main runs — even for a single-class program with no
Object.defineProperty / accessors.
- Disassembly of the hot loop confirms both
js_typed_feedback_class_field_get_guard and _set_guard are bl-called every
iteration; the class_field_inline.deref branch is gated off because the
volatile PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED byte reads non-zero.
Impact
~295 ns per class-field access (guard call + contract + TLS lookup) instead of
the slot-direct handful-of-ns the inline precheck was designed to deliver. This
affects all OOP code — the whole #5093 inline machinery is effectively dead
in normal runs.
Likely cause
PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED only flips via
disable_class_field_inline_guard(). Its callers are set_property_attrs /
set_accessor_descriptor (descriptor install) and js_gc_init (gated on
typed-feedback tracing / PERRY_VERIFY_TYPED_INTACT / PERRY_DISABLE_*, none of
which are set here). Since no env flags are set, a property descriptor is being
installed during startup (builtin/prototype setup) that trips the process-wide
global and permanently disables the inline path for the rest of the process.
The gate is monotonic and process-wide; a finer-grained gate (disable the inline
fast path only for the specific objects/prototypes that actually carry
descriptors, instead of globally) would let unrelated class instances keep the
inline fast path.
Repro
class C { value: number; constructor() { this.value = 0; } }
const c = new C();
const N = 10_000_000;
const s = Date.now();
for (let i = 0; i < N; i++) c.value = c.value + 1;
console.log(Date.now() - s); // ~2900 ms; Node ~12 ms
Relation to PR
PR perf(codegen): register typed-shape layout on standalone-constructor new path fixes a different, compounding bug (the layout was never registered on
the default new path, forcing the by-name hashmap fallback — ~2× on its own).
Once the inline precheck above is re-enabled, the typed layout that PR registers
is what the precheck reads, so the two together should close most of the
09_method_calls gap.
Summary
The codegen-inlined class-field shape guard (#5093) — the
class_field_inline.derefprecheck that letsthis.fieldread/write skip thejs_typed_feedback_class_field_*_guardcall — never engages in ordinaryprograms. Every class-field access pays a full guard call (with a thread-local
TYPED_LAYOUTSlookup) on every iteration, even for a trivial monomorphic class.Evidence
benchmarks/suite/09_method_calls.ts:PERRY_DISABLE_CLASS_FIELD_INLINE=1→ 2898 ms vs default → 2925 ms.If the inline precheck were active by default, force-disabling it would slow
the loop down; it doesn't — so the inline path is already inactive by default.
PERRY_CLASS_FIELD_INLINE_GUARD_DISABLEDshows it is set to1during runtimestartup, before
mainruns — even for a single-class program with noObject.defineProperty/ accessors.js_typed_feedback_class_field_get_guardand_set_guardarebl-called everyiteration; the
class_field_inline.derefbranch is gated off because thevolatile
PERRY_CLASS_FIELD_INLINE_GUARD_DISABLEDbyte reads non-zero.Impact
~295 ns per class-field access (guard call + contract + TLS lookup) instead of
the slot-direct handful-of-ns the inline precheck was designed to deliver. This
affects all OOP code — the whole #5093 inline machinery is effectively dead
in normal runs.
Likely cause
PERRY_CLASS_FIELD_INLINE_GUARD_DISABLEDonly flips viadisable_class_field_inline_guard(). Its callers areset_property_attrs/set_accessor_descriptor(descriptor install) andjs_gc_init(gated ontyped-feedback tracing /
PERRY_VERIFY_TYPED_INTACT/PERRY_DISABLE_*, none ofwhich are set here). Since no env flags are set, a property descriptor is being
installed during startup (builtin/prototype setup) that trips the process-wide
global and permanently disables the inline path for the rest of the process.
The gate is monotonic and process-wide; a finer-grained gate (disable the inline
fast path only for the specific objects/prototypes that actually carry
descriptors, instead of globally) would let unrelated class instances keep the
inline fast path.
Repro
Relation to PR
PR
perf(codegen): register typed-shape layout on standalone-constructor new pathfixes a different, compounding bug (the layout was never registered onthe default
newpath, forcing the by-name hashmap fallback — ~2× on its own).Once the inline precheck above is re-enabled, the typed layout that PR registers
is what the precheck reads, so the two together should close most of the
09_method_callsgap.