ZJIT: Optimize GetIvar for non-T_OBJECT

* All Invariant::SingleRactorMode PatchPoint are replaced by
  assume_single_ractor_mode() to fix https://github.com/Shopify/ruby/issues/875
  for SingleRactorMode patchpoints.
This commit is contained in:
Benoit Daloze 2025-11-25 23:26:02 +01:00
parent 2dfb8149d5
commit 07ea9a3809
Notes: git 2025-12-02 00:42:43 +00:00
7 changed files with 236 additions and 31 deletions

View File

@ -3017,7 +3017,35 @@ class TestZJIT < Test::Unit::TestCase
test
Ractor.new { test }.value
}
}, call_threshold: 2
end
def test_ivar_get_with_already_multi_ractor_mode
assert_compiles '42', %q{
class Foo
def self.set_bar
@bar = [] # needs to be a ractor unshareable object
end
def self.bar
@bar
rescue Ractor::IsolationError
42
end
end
Foo.set_bar
r = Ractor.new {
Ractor.receive
Foo.bar
}
Foo.bar
Foo.bar
r << :go
r.value
}, call_threshold: 2
end
def test_ivar_set_with_multi_ractor_mode

View File

@ -326,6 +326,7 @@ fn main() {
.allowlist_function("rb_attr_get")
.allowlist_function("rb_ivar_defined")
.allowlist_function("rb_ivar_get")
.allowlist_function("rb_ivar_get_at_no_ractor_check")
.allowlist_function("rb_ivar_set")
.allowlist_function("rb_mod_name")
.allowlist_var("rb_vm_insn_count")

View File

@ -349,6 +349,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
&Insn::Const { val: Const::Value(val) } => gen_const_value(val),
&Insn::Const { val: Const::CPtr(val) } => gen_const_cptr(val),
&Insn::Const { val: Const::CInt64(val) } => gen_const_long(val),
&Insn::Const { val: Const::CUInt16(val) } => gen_const_uint16(val),
Insn::Const { .. } => panic!("Unexpected Const in gen_insn: {insn}"),
Insn::NewArray { elements, state } => gen_new_array(asm, opnds!(elements), &function.frame_state(*state)),
Insn::NewHash { elements, state } => gen_new_hash(jit, asm, opnds!(elements), &function.frame_state(*state)),
@ -1154,6 +1155,10 @@ fn gen_const_long(val: i64) -> lir::Opnd {
Opnd::Imm(val)
}
fn gen_const_uint16(val: u16) -> lir::Opnd {
Opnd::UImm(val as u64)
}
/// Compile a basic block argument
fn gen_param(asm: &mut Assembler, idx: usize) -> lir::Opnd {
// Allocate a register or a stack slot

View File

@ -1381,6 +1381,7 @@ pub(crate) mod ids {
name: _as_heap
name: thread_ptr
name: self_ content: b"self"
name: rb_ivar_get_at_no_ractor_check
}
/// Get an CRuby `ID` to an interned string, e.g. a particular method name.

View File

@ -2034,6 +2034,7 @@ unsafe extern "C" {
pub fn rb_obj_shape_id(obj: VALUE) -> shape_id_t;
pub fn rb_shape_get_iv_index(shape_id: shape_id_t, id: ID, value: *mut attr_index_t) -> bool;
pub fn rb_shape_transition_add_ivar_no_warnings(obj: VALUE, id: ID) -> shape_id_t;
pub fn rb_ivar_get_at_no_ractor_check(obj: VALUE, index: attr_index_t) -> VALUE;
pub fn rb_gvar_get(arg1: ID) -> VALUE;
pub fn rb_gvar_set(arg1: ID, arg2: VALUE) -> VALUE;
pub fn rb_ensure_iv_list_size(obj: VALUE, current_len: u32, newsize: u32);

View File

@ -1742,6 +1742,15 @@ impl Function {
self.blocks.len()
}
pub fn assume_single_ractor_mode(&mut self, block: BlockId, state: InsnId) -> bool {
if unsafe { rb_jit_multi_ractor_p() } {
false
} else {
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::SingleRactorMode, 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.
@ -2377,6 +2386,17 @@ impl Function {
}
}
fn is_metaclass(&self, object: VALUE) -> bool {
unsafe {
if RB_TYPE_P(object, RUBY_T_CLASS) && rb_zjit_singleton_class_p(object) {
let attached = rb_class_attached_object(object);
RB_TYPE_P(attached, RUBY_T_CLASS) || RB_TYPE_P(attached, RUBY_T_MODULE)
} else {
false
}
}
}
/// Rewrite SendWithoutBlock opcodes into SendWithoutBlockDirect opcodes if we know the target
/// ISEQ statically. This removes run-time method lookups and opens the door for inlining.
/// Also try and inline constant caches, specialize object allocations, and more.
@ -2483,9 +2503,9 @@ impl Function {
// Patch points:
// Check for "defined with an un-shareable Proc in a different Ractor"
if !procv.shareable_p() {
if !procv.shareable_p() && !self.assume_single_ractor_mode(block, state) {
// TODO(alan): Turn this into a ractor belonging guard to work better in multi ractor mode.
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::SingleRactorMode, state });
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() {
@ -2498,6 +2518,12 @@ impl Function {
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args, state });
self.make_equal_to(insn_id, send_direct);
} else if def_type == VM_METHOD_TYPE_IVAR && args.is_empty() {
// Check if we're accessing ivars of a Class or Module object as they require single-ractor mode.
// We omit gen_prepare_non_leaf_call on gen_getivar, so it's unsafe to raise for multi-ractor mode.
if self.is_metaclass(klass) && !self.assume_single_ractor_mode(block, state) {
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 });
@ -2507,31 +2533,21 @@ impl Function {
}
let id = unsafe { get_cme_def_body_attr_id(cme) };
// Check if we're accessing ivars of a Class or Module object as they require single-ractor mode.
// We omit gen_prepare_non_leaf_call on gen_getivar, so it's unsafe to raise for multi-ractor mode.
if unsafe { rb_zjit_singleton_class_p(klass) } {
let attached = unsafe { rb_class_attached_object(klass) };
if unsafe { RB_TYPE_P(attached, RUBY_T_CLASS) || RB_TYPE_P(attached, RUBY_T_MODULE) } {
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::SingleRactorMode, state });
}
}
let getivar = self.push_insn(block, Insn::GetIvar { self_val: recv, id, ic: std::ptr::null(), state });
self.make_equal_to(insn_id, getivar);
} else if let (VM_METHOD_TYPE_ATTRSET, &[val]) = (def_type, args.as_slice()) {
// Check if we're accessing ivars of a Class or Module object as they require single-ractor mode.
// We omit gen_prepare_non_leaf_call on gen_getivar, so it's unsafe to raise for multi-ractor mode.
if self.is_metaclass(klass) && !self.assume_single_ractor_mode(block, state) {
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 });
}
let id = unsafe { get_cme_def_body_attr_id(cme) };
// Check if we're accessing ivars of a Class or Module object as they require single-ractor mode.
// We omit gen_prepare_non_leaf_call on gen_setivar, so it's unsafe to raise for multi-ractor mode.
if unsafe { rb_zjit_singleton_class_p(klass) } {
let attached = unsafe { rb_class_attached_object(klass) };
if unsafe { RB_TYPE_P(attached, RUBY_T_CLASS) || RB_TYPE_P(attached, RUBY_T_MODULE) } {
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::SingleRactorMode, state });
}
}
self.push_insn(block, Insn::SetIvar { self_val: recv, id, ic: std::ptr::null(), val, state });
self.make_equal_to(insn_id, val);
} else if def_type == VM_METHOD_TYPE_OPTIMIZED {
@ -2657,12 +2673,9 @@ impl Function {
self.push_insn_id(block, insn_id); continue;
}
let cref_sensitive = !unsafe { (*ice).ic_cref }.is_null();
let multi_ractor_mode = unsafe { rb_jit_multi_ractor_p() };
if cref_sensitive || multi_ractor_mode {
if cref_sensitive || !self.assume_single_ractor_mode(block, state) {
self.push_insn_id(block, insn_id); continue;
}
// Assume single-ractor mode.
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::SingleRactorMode, state });
// Invalidate output code on any constant writes associated with constants
// referenced after the PatchPoint.
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::StableConstantNames { idlist }, state });
@ -2829,11 +2842,6 @@ impl Function {
self.push_insn_id(block, insn_id); continue;
}
assert!(recv_type.shape().is_valid());
if !recv_type.flags().is_t_object() {
// Check if the receiver is a T_OBJECT
self.push_insn(block, Insn::IncrCounter(Counter::getivar_fallback_not_t_object));
self.push_insn_id(block, insn_id); continue;
}
if recv_type.shape().is_too_complex() {
// too-complex shapes can't use index access
self.push_insn(block, Insn::IncrCounter(Counter::getivar_fallback_too_complex));
@ -2847,6 +2855,17 @@ impl Function {
// entered the compiler. That means we can just return nil for this
// shape + iv name
self.push_insn(block, Insn::Const { val: Const::Value(Qnil) })
} else if !recv_type.flags().is_t_object() {
// NOTE: it's fine to use rb_ivar_get_at_no_ractor_check because
// getinstancevariable does assume_single_ractor_mode()
let ivar_index_insn: InsnId = self.push_insn(block, Insn::Const { val: Const::CUInt16(ivar_index as u16) });
self.push_insn(block, Insn::CCall {
cfunc: rb_ivar_get_at_no_ractor_check as *const u8,
recv: self_val,
args: vec![ivar_index_insn],
name: ID!(rb_ivar_get_at_no_ractor_check),
return_type: types::BasicObject,
elidable: true })
} else if recv_type.flags().is_embedded() {
// See ROBJECT_FIELDS() from include/ruby/internal/core/robject.h
let offset = ROBJECT_OFFSET_AS_ARY as i32 + (SIZEOF_VALUE * ivar_index.to_usize()) as i32;
@ -4277,6 +4296,7 @@ impl Function {
| Insn::ObjectAlloc { val, .. }
| Insn::DupArrayInclude { target: val, .. }
| Insn::GetIvar { self_val: val, .. }
| Insn::CCall { recv: val, .. }
| Insn::FixnumBitCheck { val, .. } // TODO (https://github.com/Shopify/ruby/issues/859) this should check Fixnum, but then test_checkkeyword_tests_fixnum_bit fails
| Insn::DefinedIvar { self_val: val, .. } => {
self.assert_subtype(insn_id, val, types::BasicObject)
@ -4298,7 +4318,6 @@ impl Function {
| Insn::Send { recv, ref args, .. }
| Insn::SendForward { recv, ref args, .. }
| Insn::InvokeSuper { recv, ref args, .. }
| Insn::CCall { recv, ref args, .. }
| Insn::CCallWithFrame { recv, ref args, .. }
| Insn::CCallVariadic { recv, ref args, .. }
| Insn::ArrayInclude { target: recv, elements: ref args, .. } => {
@ -5693,7 +5712,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
// ic is in arg 1
// Assume single-Ractor mode to omit gen_prepare_non_leaf_call on gen_getivar
// TODO: We only really need this if self_val is a class/module
fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::SingleRactorMode, state: exit_id });
if !fun.assume_single_ractor_mode(block, exit_id) {
// gen_getivar assumes single Ractor; side-exit into the interpreter
fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledYARVInsn(opcode) });
break; // End the block
}
let result = fun.push_insn(block, Insn::GetIvar { self_val: self_param, id, ic, state: exit_id });
state.stack_push(result);
}
@ -5702,7 +5725,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
let ic = get_arg(pc, 1).as_ptr();
// Assume single-Ractor mode to omit gen_prepare_non_leaf_call on gen_setivar
// TODO: We only really need this if self_val is a class/module
fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::SingleRactorMode, state: exit_id });
if !fun.assume_single_ractor_mode(block, exit_id) {
// gen_setivar assumes single Ractor; side-exit into the interpreter
fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledYARVInsn(opcode) });
break; // End the block
}
let val = state.stack_pop()?;
fun.push_insn(block, Insn::SetIvar { self_val: self_param, id, ic, val, state: exit_id });
}

View File

@ -5231,6 +5231,148 @@ mod hir_opt_tests {
");
}
#[test]
fn test_optimize_getivar_on_module() {
eval("
module M
@foo = 42
def self.test = @foo
end
M.test
");
assert_snapshot!(hir_string_proc("M.method(:test)"), @r"
fn test@<compiled>:4:
bb0():
EntryPoint interpreter
v1:BasicObject = LoadSelf
Jump bb2(v1)
bb1(v4:BasicObject):
EntryPoint JIT(0)
Jump bb2(v4)
bb2(v6:BasicObject):
PatchPoint SingleRactorMode
v16:HeapBasicObject = GuardType v6, HeapBasicObject
v17:HeapBasicObject = GuardShape v16, 0x1000
v18:CUInt16[0] = Const CUInt16(0)
v19:BasicObject = CCall v17, :rb_ivar_get_at_no_ractor_check@0x1008, v18
CheckInterrupts
Return v19
");
}
#[test]
fn test_optimize_getivar_on_class() {
eval("
class C
@foo = 42
def self.test = @foo
end
C.test
");
assert_snapshot!(hir_string_proc("C.method(:test)"), @r"
fn test@<compiled>:4:
bb0():
EntryPoint interpreter
v1:BasicObject = LoadSelf
Jump bb2(v1)
bb1(v4:BasicObject):
EntryPoint JIT(0)
Jump bb2(v4)
bb2(v6:BasicObject):
PatchPoint SingleRactorMode
v16:HeapBasicObject = GuardType v6, HeapBasicObject
v17:HeapBasicObject = GuardShape v16, 0x1000
v18:CUInt16[0] = Const CUInt16(0)
v19:BasicObject = CCall v17, :rb_ivar_get_at_no_ractor_check@0x1008, v18
CheckInterrupts
Return v19
");
}
#[test]
fn test_optimize_getivar_on_t_data() {
eval("
class C < Range
def test = @a
end
obj = C.new 0, 1
obj.instance_variable_set(:@a, 1)
obj.test
TEST = C.instance_method(:test)
");
assert_snapshot!(hir_string_proc("TEST"), @r"
fn test@<compiled>:3:
bb0():
EntryPoint interpreter
v1:BasicObject = LoadSelf
Jump bb2(v1)
bb1(v4:BasicObject):
EntryPoint JIT(0)
Jump bb2(v4)
bb2(v6:BasicObject):
PatchPoint SingleRactorMode
v16:HeapBasicObject = GuardType v6, HeapBasicObject
v17:HeapBasicObject = GuardShape v16, 0x1000
v18:CUInt16[0] = Const CUInt16(0)
v19:BasicObject = CCall v17, :rb_ivar_get_at_no_ractor_check@0x1008, v18
CheckInterrupts
Return v19
");
}
#[test]
fn test_optimize_getivar_on_module_multi_ractor() {
eval("
module M
@foo = 42
def self.test = @foo
end
Ractor.new {}.value
M.test
");
assert_snapshot!(hir_string_proc("M.method(:test)"), @r"
fn test@<compiled>:4:
bb0():
EntryPoint interpreter
v1:BasicObject = LoadSelf
Jump bb2(v1)
bb1(v4:BasicObject):
EntryPoint JIT(0)
Jump bb2(v4)
bb2(v6:BasicObject):
SideExit UnhandledYARVInsn(getinstancevariable)
");
}
#[test]
fn test_optimize_attr_reader_on_module_multi_ractor() {
eval("
module M
@foo = 42
class << self
attr_reader :foo
end
def self.test = foo
end
Ractor.new {}.value
M.test
");
assert_snapshot!(hir_string_proc("M.method(:test)"), @r"
fn test@<compiled>:7:
bb0():
EntryPoint interpreter
v1:BasicObject = LoadSelf
Jump bb2(v1)
bb1(v4:BasicObject):
EntryPoint JIT(0)
Jump bb2(v4)
bb2(v6:BasicObject):
v11:BasicObject = SendWithoutBlock v6, :foo
CheckInterrupts
Return v11
");
}
#[test]
fn test_dont_optimize_getivar_polymorphic() {
set_call_threshold(3);