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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions