summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBenoit Daloze <eregontp@gmail.com>2025-11-25 23:26:02 +0100
committerBenoit Daloze <eregontp@gmail.com>2025-12-02 01:42:14 +0100
commit07ea9a38097c088efd6c2872f2a563dbc69a544a (patch)
treeacb391708e56c7f7857545cc6ed5db53fba58e43
parent2dfb8149d58694cd578dbe2222640499ac021231 (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.rb30
-rw-r--r--zjit/bindgen/src/main.rs1
-rw-r--r--zjit/src/codegen.rs5
-rw-r--r--zjit/src/cruby.rs1
-rw-r--r--zjit/src/cruby_bindings.inc.rs1
-rw-r--r--zjit/src/hir.rs87
-rw-r--r--zjit/src/hir/opt_tests.rs142
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("