mirror of
https://github.com/ruby/ruby.git
synced 2026-01-27 04:24:23 +00:00
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:
parent
2dfb8149d5
commit
07ea9a3809
Notes:
git
2025-12-02 00:42:43 +00:00
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
1
zjit/src/cruby_bindings.inc.rs
generated
1
zjit/src/cruby_bindings.inc.rs
generated
@ -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);
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user