ZJIT: Add assume_no_singleton_classes to avoid invalidation loops (#15871)

Make sure we check if we have seen a singleton for this class before assuming we have not. Port the API from YJIT.
This commit is contained in:
Max Bernstein 2026-01-14 16:58:10 -05:00 committed by GitHub
parent 1ca066059f
commit b21edc1323
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
Notes: git 2026-01-14 21:58:39 +00:00
Merged-By: tekknolagi <donotemailthisaddress@bernsteinbear.com>
6 changed files with 334 additions and 229 deletions

View File

@ -857,6 +857,10 @@ fn inline_kernel_respond_to_p(
}
(_, _) => return None, // not public and include_all not known, can't compile
};
// Check singleton class assumption first, before emitting other patchpoints
if !fun.assume_no_singleton_classes(block, recv_class, state) {
return None;
}
fun.push_insn(block, hir::Insn::PatchPoint { invariant: hir::Invariant::NoTracePoint, state });
fun.push_insn(block, hir::Insn::PatchPoint {
invariant: hir::Invariant::MethodRedefined {
@ -865,11 +869,6 @@ fn inline_kernel_respond_to_p(
cme: target_cme
}, state
});
if recv_class.instance_can_have_singleton_class() {
fun.push_insn(block, hir::Insn::PatchPoint {
invariant: hir::Invariant::NoSingletonClass { klass: recv_class }, state
});
}
Some(fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(result) }))
}

View File

@ -7,7 +7,7 @@
#![allow(clippy::match_like_matches_macro)]
use crate::{
backend::lir::C_ARG_OPNDS,
cast::IntoUsize, codegen::local_idx_to_ep_offset, cruby::*, payload::{get_or_create_iseq_payload, IseqPayload}, options::{debug, get_option, DumpHIR}, state::ZJITState, json::Json
cast::IntoUsize, codegen::local_idx_to_ep_offset, cruby::*, invariants::has_singleton_class_of, payload::{get_or_create_iseq_payload, IseqPayload}, options::{debug, get_option, DumpHIR}, state::ZJITState, json::Json
};
use std::{
cell::RefCell, collections::{BTreeSet, HashMap, HashSet, VecDeque}, ffi::{c_void, c_uint, c_int, CStr}, fmt::Display, mem::{align_of, size_of}, ptr, slice::Iter
@ -644,6 +644,9 @@ pub enum SendFallbackReason {
ComplexArgPass,
/// Caller has keyword arguments but callee doesn't expect them; need to convert to hash.
UnexpectedKeywordArgs,
/// A singleton class has been seen for the receiver class, so we skip the optimization
/// to avoid an invalidation loop.
SingletonClassSeen,
/// Initial fallback reason for every instruction, which should be mutated to
/// a more actionable reason when an attempt to specialize the instruction fails.
Uncategorized(ruby_vminsn_type),
@ -680,6 +683,7 @@ impl Display for SendFallbackReason {
ArgcParamMismatch => write!(f, "Argument count does not match parameter count"),
ComplexArgPass => write!(f, "Complex argument passing"),
UnexpectedKeywordArgs => write!(f, "Unexpected Keyword Args"),
SingletonClassSeen => write!(f, "Singleton class previously created for receiver class"),
Uncategorized(insn) => write!(f, "Uncategorized({})", insn_name(*insn as usize)),
}
}
@ -1891,6 +1895,24 @@ impl Function {
}
}
/// Assume that objects of a given class will have no singleton class.
/// Returns true if safe to assume so and emits a PatchPoint.
/// Returns false if we've already seen a singleton class for this class,
/// to avoid an invalidation loop.
pub fn assume_no_singleton_classes(&mut self, block: BlockId, klass: VALUE, state: InsnId) -> bool {
if !klass.instance_can_have_singleton_class() {
// This class can never have a singleton class, so no patchpoint needed.
return true;
}
if has_singleton_class_of(klass) {
// We've seen a singleton class for this klass. Disable the optimization
// to avoid an invalidation loop.
return false;
}
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass }, state });
true
}
/// Return a copy of the instruction where the instruction and its operands have been read from
/// the union-find table (to find the current most-optimized version of this instruction). See
/// [`UnionFind`] for more.
@ -2531,8 +2553,8 @@ impl Function {
return false;
}
self.gen_patch_points_for_optimized_ccall(block, class, method_id, cme, state);
if class.instance_can_have_singleton_class() {
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass: class }, state });
if !self.assume_no_singleton_classes(block, class, state) {
return false;
}
true
}
@ -2702,10 +2724,12 @@ impl Function {
if !can_direct_send(self, block, iseq, insn_id, args.as_slice()) {
self.push_insn_id(block, insn_id); continue;
}
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state });
if klass.instance_can_have_singleton_class() {
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass }, state });
// Check singleton class assumption first, before emitting other patchpoints
if !self.assume_no_singleton_classes(block, klass, state) {
self.set_dynamic_send_reason(insn_id, SingletonClassSeen);
self.push_insn_id(block, insn_id); continue;
}
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state });
if let Some(profiled_type) = profiled_type {
recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state });
}
@ -2754,10 +2778,12 @@ impl Function {
// TODO(alan): Turn this into a ractor belonging guard to work better in multi ractor mode.
self.push_insn_id(block, insn_id); continue;
}
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state });
if klass.instance_can_have_singleton_class() {
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass }, state });
// Check singleton class assumption first, before emitting other patchpoints
if !self.assume_no_singleton_classes(block, klass, state) {
self.set_dynamic_send_reason(insn_id, SingletonClassSeen);
self.push_insn_id(block, insn_id); continue;
}
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state });
if let Some(profiled_type) = profiled_type {
recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state });
@ -2788,11 +2814,13 @@ impl Function {
if self.is_metaclass(klass) && !self.assume_single_ractor_mode(block, state) {
self.push_insn_id(block, insn_id); continue;
}
// Check singleton class assumption first, before emitting other patchpoints
if !self.assume_no_singleton_classes(block, klass, state) {
self.set_dynamic_send_reason(insn_id, SingletonClassSeen);
self.push_insn_id(block, insn_id); continue;
}
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state });
if klass.instance_can_have_singleton_class() {
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass }, state });
}
if let Some(profiled_type) = profiled_type {
recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state });
}
@ -2835,10 +2863,12 @@ impl Function {
// No (monomorphic/skewed polymorphic) profile info
self.push_insn_id(block, insn_id); continue;
};
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state });
if klass.instance_can_have_singleton_class() {
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass }, state });
// Check singleton class assumption first, before emitting other patchpoints
if !self.assume_no_singleton_classes(block, klass, state) {
self.set_dynamic_send_reason(insn_id, SingletonClassSeen);
self.push_insn_id(block, insn_id); continue;
}
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state });
if let Some(profiled_type) = profiled_type {
recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state });
}
@ -3375,11 +3405,14 @@ impl Function {
return Err(());
}
// Check singleton class assumption first, before emitting other patchpoints
if !fun.assume_no_singleton_classes(block, recv_class, state) {
fun.set_dynamic_send_reason(send_insn_id, SingletonClassSeen);
return Err(());
}
// Commit to the replacement. Put PatchPoint.
fun.gen_patch_points_for_optimized_ccall(block, recv_class, method_id, cme, state);
if recv_class.instance_can_have_singleton_class() {
fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass: recv_class }, state });
}
if let Some(profiled_type) = profiled_type {
// Guard receiver class
@ -3410,11 +3443,15 @@ impl Function {
-1 => {
// The method gets a pointer to the first argument
// func(int argc, VALUE *argv, VALUE recv)
// Check singleton class assumption first, before emitting other patchpoints
if !fun.assume_no_singleton_classes(block, recv_class, state) {
fun.set_dynamic_send_reason(send_insn_id, SingletonClassSeen);
return Err(());
}
fun.gen_patch_points_for_optimized_ccall(block, recv_class, method_id, cme, state);
if recv_class.instance_can_have_singleton_class() {
fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass: recv_class }, state });
}
if let Some(profiled_type) = profiled_type {
// Guard receiver class
recv = fun.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state });
@ -3522,11 +3559,14 @@ impl Function {
return Err(());
}
// Check singleton class assumption first, before emitting other patchpoints
if !fun.assume_no_singleton_classes(block, recv_class, state) {
fun.set_dynamic_send_reason(send_insn_id, SingletonClassSeen);
return Err(());
}
// Commit to the replacement. Put PatchPoint.
fun.gen_patch_points_for_optimized_ccall(block, recv_class, method_id, cme, state);
if recv_class.instance_can_have_singleton_class() {
fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass: recv_class }, state });
}
let props = ZJITState::get_method_annotations().get_cfunc_properties(cme);
if props.is_none() && get_option!(stats) {
@ -3599,11 +3639,14 @@ impl Function {
fun.set_dynamic_send_reason(send_insn_id, ComplexArgPass);
return Err(());
} else {
// Check singleton class assumption first, before emitting other patchpoints
if !fun.assume_no_singleton_classes(block, recv_class, state) {
fun.set_dynamic_send_reason(send_insn_id, SingletonClassSeen);
return Err(());
}
fun.gen_patch_points_for_optimized_ccall(block, recv_class, method_id, cme, state);
if recv_class.instance_can_have_singleton_class() {
fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass: recv_class }, state });
}
if let Some(profiled_type) = profiled_type {
// Guard receiver class
recv = fun.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state });

File diff suppressed because it is too large Load Diff

View File

@ -76,8 +76,8 @@ mod snapshot_tests {
v13:Fixnum[1] = Const Value(1)
v15:Fixnum[2] = Const Value(2)
v16:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13, v15], locals: [] }
PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020)
PatchPoint NoSingletonClass(Object@0x1010)
PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020)
v24:HeapObject[class_exact*:Object@VALUE(0x1010)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1010)]
v25:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v13, v15, v11], locals: [] }
v26:BasicObject = SendWithoutBlockDirect v24, :foo (0x1048), v13, v15, v11
@ -111,8 +111,8 @@ mod snapshot_tests {
v11:Fixnum[1] = Const Value(1)
v13:Fixnum[2] = Const Value(2)
v14:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13], locals: [] }
PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020)
PatchPoint NoSingletonClass(Object@0x1010)
PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020)
v22:HeapObject[class_exact*:Object@VALUE(0x1010)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1010)]
v23:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13], locals: [] }
v24:BasicObject = SendWithoutBlockDirect v22, :foo (0x1048), v11, v13

View File

@ -436,6 +436,16 @@ pub extern "C" fn rb_zjit_tracing_invalidate_all() {
});
}
/// Returns true if we've seen a singleton class of a given class since boot.
/// This is used to avoid an invalidation loop where we repeatedly compile code
/// that assumes no singleton class, only to have it invalidated.
pub fn has_singleton_class_of(klass: VALUE) -> bool {
ZJITState::get_invariants()
.no_singleton_class_patch_points
.get(&klass)
.map_or(false, |patch_points| patch_points.is_empty())
}
#[unsafe(no_mangle)]
pub extern "C" fn rb_zjit_invalidate_no_singleton_class(klass: VALUE) {
if !zjit_enabled_p() {
@ -444,11 +454,22 @@ pub extern "C" fn rb_zjit_invalidate_no_singleton_class(klass: VALUE) {
with_vm_lock(src_loc!(), || {
let invariants = ZJITState::get_invariants();
if let Some(patch_points) = invariants.no_singleton_class_patch_points.remove(&klass) {
let cb = ZJITState::get_code_block();
debug!("Singleton class created for {:?}", klass);
compile_patch_points!(cb, patch_points, "Singleton class created for {:?}", klass);
cb.mark_all_executable();
match invariants.no_singleton_class_patch_points.get_mut(&klass) {
Some(patch_points) => {
// Invalidate existing patch points and let has_singleton_class_of()
// return true when they are compiled again
let patch_points = mem::take(patch_points);
if !patch_points.is_empty() {
let cb = ZJITState::get_code_block();
debug!("Singleton class created for {:?}", klass);
compile_patch_points!(cb, patch_points, "Singleton class created for {:?}", klass);
cb.mark_all_executable();
}
}
None => {
// Let has_singleton_class_of() return true for this class
invariants.no_singleton_class_patch_points.insert(klass, HashSet::new());
}
}
});
}

View File

@ -241,6 +241,8 @@ make_counters! {
send_fallback_one_or_more_complex_arg_pass,
// Caller has keyword arguments but callee doesn't expect them.
send_fallback_unexpected_keyword_args,
// Singleton class previously created for receiver class.
send_fallback_singleton_class_seen,
send_fallback_bmethod_non_iseq_proc,
send_fallback_obj_to_string_not_string,
send_fallback_send_cfunc_variadic,
@ -573,6 +575,7 @@ pub fn send_fallback_counter(reason: crate::hir::SendFallbackReason) -> Counter
SendCfuncArrayVariadic => send_fallback_send_cfunc_array_variadic,
ComplexArgPass => send_fallback_one_or_more_complex_arg_pass,
UnexpectedKeywordArgs => send_fallback_unexpected_keyword_args,
SingletonClassSeen => send_fallback_singleton_class_seen,
ArgcParamMismatch => send_fallback_argc_param_mismatch,
BmethodNonIseqProc => send_fallback_bmethod_non_iseq_proc,
SendNotOptimizedMethodType(_) => send_fallback_send_not_optimized_method_type,