mirror of
https://github.com/ruby/ruby.git
synced 2026-01-26 12:14:51 +00:00
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:
parent
1ca066059f
commit
b21edc1323
Notes:
git
2026-01-14 21:58:39 +00:00
Merged-By: tekknolagi <donotemailthisaddress@bernsteinbear.com>
@ -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) }))
|
||||
}
|
||||
|
||||
|
||||
@ -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
@ -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
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user