Skip to content

perf: class-field inline fast path (#5093) is disabled process-wide in normal runs #5654

Description

@TheHypnoo

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions