diff options
| author | Benoit Daloze <eregontp@gmail.com> | 2025-11-25 23:26:02 +0100 |
|---|---|---|
| committer | Benoit Daloze <eregontp@gmail.com> | 2025-12-02 01:42:14 +0100 |
| commit | 07ea9a38097c088efd6c2872f2a563dbc69a544a (patch) | |
| tree | acb391708e56c7f7857545cc6ed5db53fba58e43 | |
| parent | 2dfb8149d58694cd578dbe2222640499ac021231 (diff) | |
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.
| -rw-r--r-- | test/ruby/test_zjit.rb | 30 | ||||
| -rw-r--r-- | zjit/bindgen/src/main.rs | 1 | ||||
| -rw-r--r-- | zjit/src/codegen.rs | 5 | ||||
| -rw-r--r-- | zjit/src/cruby.rs | 1 | ||||
| -rw-r--r-- | zjit/src/cruby_bindings.inc.rs | 1 | ||||
| -rw-r--r-- | zjit/src/hir.rs | 87 | ||||
| -rw-r--r-- | zjit/src/hir/opt_tests.rs | 142 |
7 files changed, 236 insertions, 31 deletions
diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index d821d8ad5c..6609bb2461 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -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 diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 6659049242..0860c073cd 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -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") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index ac1c65b26e..bb19d7d820 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -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 diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 72c44ccc6e..9ed3a1abf7 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -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. diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index fb329a0716..7bd3925639 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -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); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 49f139b45e..5ede64d42c 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -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 }); } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index bdc26f758e..9809c8bffb 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -5232,6 +5232,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); eval(" |
