From c0bf51fbca8d3eb4655351117ab09df2e68bbc63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 17 Jun 2026 22:12:22 +0200 Subject: [PATCH] perf(codegen): outline class-field-SET guard-miss arm to one call (#5334 lever A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default class-field-set diamond runs the inline `js_typed_feedback_class_field_set_guard` in its entry block; on a guard PASS it stores the slot inline, on a MISS it branches to the fallback arm. That arm emitted TWO inline calls per set site — `js_typed_feedback_record_fallback_call` then `js_object_set_field_by_name`. Since the guard has already run and FAILED (that failure is what branched control here), nothing is left to decide: collapse the pair into a single outlined `js_class_field_set_fallback(site_id, obj_bits, key_raw, value)` that records the miss and routes the write by name. Perf-neutral by construction: the hot `class_field_set.fast` slot store is untouched, and the change is confined to the cold guard-miss arm, which never executes on a monomorphic hot path. IR shrinks by one call per class-field-SET site (verified on emitted IR: fallback arm 2 calls -> 1; full perry-codegen suite green). First step of the IR-efficiency roadmap in #5334 (Tier 1, lever A: outline cold IC machinery). Establishes the outline-helper pattern reused by the larger levers. --- crates/perry-codegen/src/expr/property_set.rs | 19 ++++++++-- .../src/runtime_decls/objects.rs | 8 ++++ crates/perry-codegen/tests/typed_feedback.rs | 15 +++++++- .../src/typed_feedback/guards.rs | 37 +++++++++++++++++++ 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/crates/perry-codegen/src/expr/property_set.rs b/crates/perry-codegen/src/expr/property_set.rs index ac9acf264..2936d8b4f 100644 --- a/crates/perry-codegen/src/expr/property_set.rs +++ b/crates/perry-codegen/src/expr/property_set.rs @@ -436,10 +436,23 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ctx.current_block = fallback_idx; let blk = ctx.block(); - blk.call_void("js_typed_feedback_record_fallback_call", &[(I64, &site_id)]); + // #5334 lever A: the guard already ran and FAILED in the + // entry block, so this cold arm is a pure guard-miss + // fallback. Outline the two operations it used to emit + // inline (record_fallback + by-name set) into ONE + // `js_class_field_set_fallback` call. Semantics are + // byte-identical; only the emitted IR shrinks (cold path + // → zero hot-loop cost). `obj_bits` keeps the full + // NaN-box tag; `key_raw` is POINTER_MASK-stripped — the + // same operands the two calls received. blk.call_void( - "js_object_set_field_by_name", - &[(I64, &obj_bits), (I64, &key_raw), (DOUBLE, &val_double)], + "js_class_field_set_fallback", + &[ + (I64, &site_id), + (I64, &obj_bits), + (I64, &key_raw), + (DOUBLE, &val_double), + ], ); blk.br(&merge_label); if requires_raw_f64 { diff --git a/crates/perry-codegen/src/runtime_decls/objects.rs b/crates/perry-codegen/src/runtime_decls/objects.rs index 8bbfa981d..e10916c27 100644 --- a/crates/perry-codegen/src/runtime_decls/objects.rs +++ b/crates/perry-codegen/src/runtime_decls/objects.rs @@ -112,6 +112,14 @@ pub fn declare_phase_b_objects(module: &mut LlModule) { I32, &[I64, DOUBLE, I32, I64, I64, I32, DOUBLE, I32], ); + // #5334 lever A: class-field-SET guard-MISS fallback, outlined. The cold arm + // of the default diamond collapses from two calls (record_fallback + + // set_field_by_name) to this one. Args: (site_id, obj_bits, key_raw, value). + module.declare_function( + "js_class_field_set_fallback", + VOID, + &[I64, I64, I64, DOUBLE], + ); module.declare_function( "js_typed_feedback_class_field_get_guard", I32, diff --git a/crates/perry-codegen/tests/typed_feedback.rs b/crates/perry-codegen/tests/typed_feedback.rs index 8543cadf2..9ad0fad33 100644 --- a/crates/perry-codegen/tests/typed_feedback.rs +++ b/crates/perry-codegen/tests/typed_feedback.rs @@ -296,8 +296,14 @@ fn typed_feedback_guards_direct_class_field_specialization() { assert!(ir.contains("class_field_get.fallback")); assert!(ir.contains("store double")); assert!(!ir.contains("call void @js_gc_note_slot_layout")); + // #5334 lever A: the SET fallback arm collapses to one outlined call; the + // by-name SET it replaced is no longer emitted at the set site. + assert!(ir.contains("call void @js_class_field_set_fallback")); + assert!(!ir.contains("call void @js_object_set_field_by_name")); + // `record_fallback_call` is still present — but from the class-field-GET + // fallback block below, not the SET site (the SET copy is now folded into + // js_class_field_set_fallback). assert!(ir.contains("call void @js_typed_feedback_record_fallback_call")); - assert!(ir.contains("call void @js_object_set_field_by_name")); assert!(ir.contains("call double @js_object_get_field_by_name_f64")); } @@ -340,7 +346,12 @@ fn typed_feedback_guards_direct_class_method_specialization() { assert!(ir.contains("js_typed_feedback_method_direct_call_guard")); assert!(ir.contains("method_direct.fast")); assert!(ir.contains("method_direct.fallback")); - assert!(ir.contains("call void @js_typed_feedback_record_fallback_call")); + // #5334 lever A: this class has a field `x` whose synthesized field-set + // routes its guard-miss arm through the outlined fallback. (The + // method-direct fallback only records when its site_id is Some, which it + // isn't here — the old `record_fallback_call` assertion was incidentally + // satisfied by the field-set fallback that is now folded into this call.) + assert!(ir.contains("call void @js_class_field_set_fallback")); assert!(ir.contains("call double @js_native_call_method")); } diff --git a/crates/perry-runtime/src/typed_feedback/guards.rs b/crates/perry-runtime/src/typed_feedback/guards.rs index 09fe2ad0b..2fabad649 100644 --- a/crates/perry-runtime/src/typed_feedback/guards.rs +++ b/crates/perry-runtime/src/typed_feedback/guards.rs @@ -599,6 +599,42 @@ pub extern "C" fn js_typed_feedback_class_field_set_guard( } } +/// Class-field-SET guard-MISS fallback, outlined (#5334, lever A). +/// +/// The default class-field-set diamond runs the inline +/// `js_typed_feedback_class_field_set_guard` in its entry block; on a guard +/// PASS it stores the slot inline, on a MISS it branches to the fallback arm. +/// That arm used to emit TWO inline calls per set site — +/// `js_typed_feedback_record_fallback_call` then `js_object_set_field_by_name`. +/// Since the guard has already run and FAILED (that failure is what branched +/// control here), nothing is left to decide: this helper just reproduces those +/// two operations, collapsed into ONE call so the cold arm costs a single +/// instruction per site instead of two. +/// +/// Byte-identical semantics to the old inline pair: +/// 1. record the miss for typed feedback, then +/// 2. route the write by name (handles frozen / accessor / non-writable / +/// setter-in-chain). `obj_bits` keeps the full NaN-box tag (the by-name +/// setter inspects it for proxy/exotic dispatch before masking to the +/// heap address); `key_raw` is the POINTER_MASK-stripped key handle. +/// +/// Cold-path only, so the extra call frame has zero hot-loop cost; the win is +/// purely in emitted IR size. +#[no_mangle] +pub extern "C" fn js_class_field_set_fallback( + site_id: u64, + obj_bits: u64, + key_raw: u64, + value: f64, +) { + crate::typed_feedback::js_typed_feedback_record_fallback_call(site_id); + crate::object::js_object_set_field_by_name( + obj_bits as *mut ObjectHeader, + key_raw as *const crate::StringHeader, + value, + ); +} + #[no_mangle] pub unsafe extern "C" fn js_typed_feedback_native_call_method( site_id: u64, @@ -835,6 +871,7 @@ mod keep_guard_symbols { use super::*; #[used] static G0: extern "C" fn(u64, f64, u32, *const ArrayHeader, *const crate::StringHeader, u32, i32) -> i32 = js_typed_feedback_class_field_get_guard; #[used] static G1: extern "C" fn(u64, f64, u32, *const ArrayHeader, *const crate::StringHeader, u32, f64, i32) -> i32 = js_typed_feedback_class_field_set_guard; + #[used] static G1C: extern "C" fn(u64, u64, u64, f64) = js_class_field_set_fallback; #[used] static G2: unsafe extern "C" fn(u64, f64, u32, *const ArrayHeader, *const i8, usize, *const u8) -> i32 = js_typed_feedback_method_direct_call_guard; #[used] static G3: extern "C" fn(u64, f64, *const u8, u32, u32) -> i32 = js_typed_feedback_closure_direct_call_guard; }