diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 683d53f339f8c2..937cf44e190a56 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -1642,7 +1642,7 @@ def test(c) = c.foo }, call_threshold: 2, insns: [:opt_send_without_block] end - def test_attr_accessor + def test_attr_accessor_getivar assert_compiles '[4, 4]', %q{ class C attr_accessor :foo @@ -1658,6 +1658,47 @@ def test(c) = c.foo }, call_threshold: 2, insns: [:opt_send_without_block] end + def test_attr_accessor_setivar + assert_compiles '[5, 5]', %q{ + class C + attr_accessor :foo + + def initialize + @foo = 4 + end + end + + def test(c) + c.foo = 5 + c.foo + end + + c = C.new + [test(c), test(c)] + }, call_threshold: 2, insns: [:opt_send_without_block] + end + + def test_attr_writer + assert_compiles '[5, 5]', %q{ + class C + attr_writer :foo + + def initialize + @foo = 4 + end + + def get_foo = @foo + end + + def test(c) + c.foo = 5 + c.get_foo + end + c = C.new + [test(c), test(c)] + }, call_threshold: 2, insns: [:opt_send_without_block] + end + def test_uncached_getconstant_path assert_compiles RUBY_COPYRIGHT.dump, %q{ def test = RUBY_COPYRIGHT diff --git a/zjit.rb b/zjit.rb index b85b5629a72654..a46802553c274a 100644 --- a/zjit.rb +++ b/zjit.rb @@ -53,6 +53,8 @@ def stats_string # Show the most important stats ratio_in_zjit at the end print_counters([ :dynamic_send_count, + :dynamic_getivar_count, + :dynamic_setivar_count, :compiled_iseq_count, :failed_iseq_count, diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index e8f1d968298860..7676d7eed46e70 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -715,11 +715,13 @@ fn gen_ccall_variadic( /// Emit an uncached instance variable lookup fn gen_getivar(asm: &mut Assembler, recv: Opnd, id: ID) -> Opnd { + gen_incr_counter(asm, Counter::dynamic_getivar_count); asm_ccall!(asm, rb_ivar_get, recv, id.0.into()) } /// Emit an uncached instance variable store fn gen_setivar(asm: &mut Assembler, recv: Opnd, id: ID, val: Opnd) { + gen_incr_counter(asm, Counter::dynamic_setivar_count); asm_ccall!(asm, rb_ivar_set, recv, id.0.into(), val); } diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index d81231e2820b88..824b35f7673dc3 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1986,6 +1986,24 @@ impl Function { } let getivar = self.push_insn(block, Insn::GetIvar { self_val: recv, id, state }); self.make_equal_to(insn_id, getivar); + } else if def_type == VM_METHOD_TYPE_ATTRSET && args.len() == 1 { + 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 }); + } + } + let val = args[0]; + let setivar = self.push_insn(block, Insn::SetIvar { self_val: recv, id, val, state }); + self.make_equal_to(insn_id, setivar); } else { if let Insn::SendWithoutBlock { def_type: insn_def_type, .. } = &mut self.insns[insn_id.0] { *insn_def_type = Some(MethodType::from(def_type)); @@ -12133,4 +12151,66 @@ mod opt_tests { Return v25 "); } + + #[test] + fn test_inline_attr_accessor_set() { + eval(" + class C + attr_accessor :foo + end + + def test(o) = o.foo = 5 + test C.new + test C.new + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:6: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + v14:Fixnum[5] = Const Value(5) + PatchPoint MethodRedefined(C@0x1000, foo=@0x1008, cme:0x1010) + v23:HeapObject[class_exact:C] = GuardType v9, HeapObject[class_exact:C] + SetIvar v23, :@foo, v14 + CheckInterrupts + Return v14 + "); + } + + #[test] + fn test_inline_attr_writer_set() { + eval(" + class C + attr_writer :foo + end + + def test(o) = o.foo = 5 + test C.new + test C.new + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:6: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + v14:Fixnum[5] = Const Value(5) + PatchPoint MethodRedefined(C@0x1000, foo=@0x1008, cme:0x1010) + v23:HeapObject[class_exact:C] = GuardType v9, HeapObject[class_exact:C] + SetIvar v23, :@foo, v14 + CheckInterrupts + Return v14 + "); + } } diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 42dc44538ada0a..7329b3442af0df 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -142,6 +142,10 @@ make_counters! { dynamic_send_type_invokeblock, dynamic_send_type_invokesuper, + // The number of times we do a dynamic ivar lookup from JIT code + dynamic_getivar_count, + dynamic_setivar_count, + // Method call def_type related to fallback to dynamic dispatch unspecialized_def_type_iseq, unspecialized_def_type_cfunc,