Skip to content

Commit 83447d5

Browse files
committed
ZJIT: Recompile getivar on guard_shape_failure
1 parent 0fde80f commit 83447d5

5 files changed

Lines changed: 190 additions & 40 deletions

File tree

zjit/src/backend/lir.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -557,12 +557,14 @@ pub struct SideExit {
557557
pub recompile: Option<SideExitRecompile>,
558558
}
559559

560-
/// Arguments for the no-profile-send recompile callback.
560+
/// Arguments for the recompile callback on side exit.
561+
/// Used for both no-profile sends (argc >= 0) and shape guard failures (argc = -1).
561562
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
562563
pub struct SideExitRecompile {
563564
pub iseq: Opnd,
564565
pub insn_idx: u32,
565-
/// Number of arguments, not including the receiver.
566+
/// Number of arguments (not including receiver) for send profiling.
567+
/// -1 means profile self from CFP for shape guard exits.
566568
pub argc: i32,
567569
}
568570

@@ -2671,17 +2673,17 @@ impl Assembler
26712673
if let Some(recompile) = &exit.recompile {
26722674
if cfg!(feature = "runtime_checks") {
26732675
// Clear jit_return to fully materialize the frame. This must happen
2674-
// before any C call in the exit path (e.g. no_profile_send_recompile)
2676+
// before any C call in the exit path (e.g. exit_recompile)
26752677
// because that C call can trigger GC, which walks the stack and would
26762678
// hit the CFP_JIT_RETURN assertion if jit_return still holds the
26772679
// runtime_checks poison value (JIT_RETURN_POISON).
26782680
asm_comment!(asm, "clear cfp->jit_return");
26792681
asm.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_JIT_RETURN), 0.into());
26802682
}
26812683

2682-
use crate::codegen::no_profile_send_recompile;
2683-
asm_comment!(asm, "profile and maybe recompile for no-profile send");
2684-
asm_ccall!(asm, no_profile_send_recompile,
2684+
use crate::codegen::exit_recompile;
2685+
asm_comment!(asm, "profile and maybe recompile");
2686+
asm_ccall!(asm, exit_recompile,
26852687
EC,
26862688
recompile.iseq,
26872689
Opnd::UImm(recompile.insn_idx as u64),

zjit/src/codegen.rs

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func
518518
Insn::InvokeBuiltin { .. } => SideExitReason::UnhandledHIRInvokeBuiltin,
519519
_ => SideExitReason::UnhandledHIRUnknown(insn_id),
520520
};
521-
gen_side_exit(&mut jit, &mut asm, &reason, &None, &function.frame_state(last_snapshot));
521+
gen_side_exit(&mut jit, &mut asm, &reason, None, &function.frame_state(last_snapshot));
522522
// Don't bother generating code after a side-exit. We won't run it.
523523
// TODO(max): Generate ud2 or equivalent.
524524
break;
@@ -689,7 +689,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
689689
Insn::HasType { val, expected } => gen_has_type(jit, asm, opnd!(val), *expected),
690690
Insn::GuardType { val, guard_type, state } => gen_guard_type(jit, asm, opnd!(val), *guard_type, &function.frame_state(*state)),
691691
Insn::GuardTypeNot { val, guard_type, state } => gen_guard_type_not(jit, asm, opnd!(val), *guard_type, &function.frame_state(*state)),
692-
&Insn::GuardBitEquals { val, expected, reason, state } => gen_guard_bit_equals(jit, asm, opnd!(val), expected, reason, &function.frame_state(state)),
692+
&Insn::GuardBitEquals { val, expected, reason, state, recompile } => gen_guard_bit_equals(jit, asm, opnd!(val), expected, reason, recompile, &function.frame_state(state)),
693693
&Insn::GuardAnyBitSet { val, mask, reason, state, .. } => gen_guard_any_bit_set(jit, asm, opnd!(val), mask, reason, &function.frame_state(state)),
694694
&Insn::GuardNoBitsSet { val, mask, reason, state, .. } => gen_guard_no_bits_set(jit, asm, opnd!(val), mask, reason, &function.frame_state(state)),
695695
&Insn::GuardLess { left, right, state } => gen_guard_less(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)),
@@ -717,7 +717,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
717717
Insn::SetClassVar { id, val, ic, state } => no_output!(gen_setclassvar(jit, asm, *id, opnd!(val), *ic, &function.frame_state(*state))),
718718
Insn::SetIvar { self_val, id, ic, val, state } => no_output!(gen_setivar(jit, asm, opnd!(self_val), *id, *ic, opnd!(val), &function.frame_state(*state))),
719719
Insn::FixnumBitCheck { val, index } => gen_fixnum_bit_check(asm, opnd!(val), *index),
720-
Insn::SideExit { state, reason, recompile } => no_output!(gen_side_exit(jit, asm, reason, recompile, &function.frame_state(*state))),
720+
Insn::SideExit { state, reason, recompile } => no_output!(gen_side_exit(jit, asm, reason, *recompile, &function.frame_state(*state))),
721721
Insn::PutSpecialObject { value_type } => gen_putspecialobject(asm, *value_type),
722722
Insn::AnyToString { val, str, state } => gen_anytostring(asm, opnd!(val), opnd!(str), &function.frame_state(*state)),
723723
Insn::Defined { op_type, obj, pushval, v, state } => gen_defined(jit, asm, *op_type, *obj, *pushval, opnd!(v), &function.frame_state(*state)),
@@ -1238,14 +1238,8 @@ fn gen_setglobal(jit: &mut JITState, asm: &mut Assembler, id: ID, val: Opnd, sta
12381238
}
12391239

12401240
/// Side-exit into the interpreter
1241-
fn gen_side_exit(jit: &mut JITState, asm: &mut Assembler, reason: &SideExitReason, recompile: &Option<i32>, state: &FrameState) {
1242-
let mut exit = build_side_exit(jit, state);
1243-
exit.recompile = recompile.map(|argc| SideExitRecompile {
1244-
iseq: Opnd::Value(VALUE::from(jit.iseq)),
1245-
insn_idx: state.insn_idx() as u32,
1246-
argc,
1247-
});
1248-
asm.jmp(Target::SideExit { exit, reason: *reason });
1241+
fn gen_side_exit(jit: &mut JITState, asm: &mut Assembler, reason: &SideExitReason, recompile: Option<i32>, state: &FrameState) {
1242+
asm.jmp(side_exit_with_recompile(jit, state, *reason, recompile));
12491243
}
12501244

12511245
/// Emit a special object lookup
@@ -2637,7 +2631,7 @@ fn gen_guard_type_not(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, g
26372631
}
26382632

26392633
/// Compile an identity check with a side exit
2640-
fn gen_guard_bit_equals(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, expected: crate::hir::Const, reason: SideExitReason, state: &FrameState) -> lir::Opnd {
2634+
fn gen_guard_bit_equals(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, expected: crate::hir::Const, reason: SideExitReason, recompile: Option<i32>, state: &FrameState) -> lir::Opnd {
26412635
if matches!(reason, SideExitReason::GuardShape(_) ) {
26422636
gen_incr_counter(asm, Counter::guard_shape_count);
26432637
}
@@ -2648,7 +2642,7 @@ fn gen_guard_bit_equals(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd,
26482642
_ => panic!("gen_guard_bit_equals: unexpected hir::Const {expected:?}"),
26492643
};
26502644
asm.cmp(val, expected_opnd);
2651-
asm.jnz(jit, side_exit(jit, state, reason));
2645+
asm.jnz(jit, side_exit_with_recompile(jit, state, reason, recompile));
26522646
val
26532647
}
26542648

@@ -2982,6 +2976,18 @@ fn side_exit(jit: &JITState, state: &FrameState, reason: SideExitReason) -> Targ
29822976
Target::SideExit { exit, reason }
29832977
}
29842978

2979+
/// Build a Target::SideExit that optionally triggers exit_recompile on the exit path.
2980+
/// When `recompile` is Some(argc), the side exit calls exit_recompile with that argc.
2981+
fn side_exit_with_recompile(jit: &JITState, state: &FrameState, reason: SideExitReason, recompile: Option<i32>) -> Target {
2982+
let mut exit = build_side_exit(jit, state);
2983+
exit.recompile = recompile.map(|argc| SideExitRecompile {
2984+
iseq: Opnd::Value(VALUE::from(jit.iseq)),
2985+
insn_idx: state.insn_idx() as u32,
2986+
argc,
2987+
});
2988+
Target::SideExit { exit, reason }
2989+
}
2990+
29852991
/// Build a side-exit context
29862992
fn build_side_exit(jit: &JITState, state: &FrameState) -> SideExit {
29872993
let mut stack = Vec::new();
@@ -3037,26 +3043,48 @@ macro_rules! c_callable {
30373043
pub(crate) use c_callable;
30383044

30393045
c_callable! {
3040-
/// Called from JIT side-exit code when a send instruction had no profile data. This function
3041-
/// profiles the receiver and arguments on the stack, then (once enough profiles are gathered)
3042-
/// invalidates the current ISEQ version so that the ISEQ will be recompiled with the new
3043-
/// profile data on the next call.
3044-
pub(crate) fn no_profile_send_recompile(ec: EcPtr, iseq_raw: VALUE, insn_idx: u32, argc: i32) {
3046+
/// Called from JIT side-exit code to profile operands and trigger recompilation.
3047+
/// For send instructions (argc >= 0): profiles receiver + args from the stack.
3048+
/// For shape guard exits (argc == -1): profiles self from the CFP.
3049+
/// Once enough profiles are gathered, invalidates the ISEQ for recompilation.
3050+
pub(crate) fn exit_recompile(ec: EcPtr, iseq_raw: VALUE, insn_idx: u32, argc: i32) {
3051+
// Fast check before taking the VM lock: skip if already invalidated or
3052+
// at the version limit. This avoids expensive lock acquisition on every
3053+
// shape guard exit after the recompile has already been triggered.
3054+
{
3055+
let iseq: IseqPtr = iseq_raw.as_iseq();
3056+
let payload = get_or_create_iseq_payload(iseq);
3057+
let already_done = payload.versions.last()
3058+
.map_or(false, |v| unsafe { v.as_ref() }.status == IseqStatus::Invalidated)
3059+
|| payload.versions.len() >= max_iseq_versions();
3060+
if already_done {
3061+
return;
3062+
}
3063+
}
3064+
30453065
with_vm_lock(src_loc!(), || {
30463066
let iseq: IseqPtr = iseq_raw.as_iseq();
30473067
let payload = get_or_create_iseq_payload(iseq);
30483068

3049-
// Already gathered enough profiles; nothing to do
3050-
if payload.profile.done_profiling_at(insn_idx as usize) {
3069+
// For no-profile sends, skip if already profiled at this insn_idx.
3070+
// For shape guard exits (argc == -1), always re-profile because the
3071+
// original YARV profiles were monomorphic but runtime showed new shapes.
3072+
if argc >= 0 && payload.profile.done_profiling_at(insn_idx as usize) {
30513073
return;
30523074
}
30533075

30543076
with_time_stat(Counter::profile_time_ns, || {
30553077
let cfp = unsafe { get_ec_cfp(ec) };
3056-
let sp = unsafe { get_cfp_sp(cfp) };
30573078

3058-
// Profile the receiver and arguments for this send instruction
3059-
let should_recompile = payload.profile.profile_send_at(iseq, insn_idx as usize, sp, argc as usize);
3079+
let should_recompile = if argc >= 0 {
3080+
let sp = unsafe { get_cfp_sp(cfp) };
3081+
// Profile the receiver and arguments for this send instruction
3082+
payload.profile.profile_send_at(iseq, insn_idx as usize, sp, argc as usize)
3083+
} else {
3084+
// Profile self for shape guard exits (argc == -1)
3085+
let self_val = unsafe { get_cfp_self(cfp) };
3086+
payload.profile.profile_self_at(iseq, insn_idx as usize, self_val)
3087+
};
30603088

30613089
// Once we have enough profiles, invalidate and recompile the ISEQ
30623090
if should_recompile {

0 commit comments

Comments
 (0)