From 378c4db4bbaf0c9ecd954a510a14c617a4ba4908 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Tue, 16 Sep 2025 15:48:18 -0400 Subject: [PATCH 1/8] [DOC] Add `snt` and `dnt` to glossary.md --- doc/contributing/glossary.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/contributing/glossary.md b/doc/contributing/glossary.md index 6f9c33502820ce..fb5570f5e2562b 100644 --- a/doc/contributing/glossary.md +++ b/doc/contributing/glossary.md @@ -33,6 +33,8 @@ Just a list of acronyms I've run across in the Ruby source code and their meanin | `me` | Method Entry. Refers to an `rb_method_entry_t` struct, the internal representation of a Ruby method. | | MRI | Matz's Ruby Implementation | | `pc` | Program Counter. Usually the instruction that will be executed _next_ by the VM. Pointed to by the `cfp` and incremented by the VM | +| `snt` | Shared Native Thread. OS thread on which many ruby threads can run. Ruby threads from different ractors can even run on the same SNT. Ruby threads can switch SNTs when they context switch. SNTs are used in the M:N threading model. By default, non-main ractors use this model. +| `dnt` | Dedicated Native Thread. OS thread on which only one ruby thread can run. The ruby thread always runs on that same OS thread. DNTs are used in the 1:1 threading model. By default, the main ractor uses this model. | `sp` | Stack Pointer. The top of the stack. The VM executes instructions in the `iseq` and instructions will push and pop values on the stack. The VM updates the `sp` on the `cfp` to point at the top of the stack| | `svar` | Special Variable. Refers to special local variables like `$~` and `$_`. See the `getspecial` instruction in `insns.def` | | `VALUE` | VALUE is a pointer to a ruby object from the Ruby C code. | From 133e3889c1f77ca1eeedb49e19cca460f1dfa785 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Mon, 22 Sep 2025 20:39:57 +0100 Subject: [PATCH 2/8] [ruby/erb] [DOC] Correct two errors https://github.com/ruby/erb/commit/1512314a3b --- lib/erb.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/erb.rb b/lib/erb.rb index de490c089e3a9f..b9b4ef7a1deec3 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -1173,12 +1173,12 @@ def def_module(methodname='erb') # template = ERB.new(html) # ``` # - # Create a base class that has `@arg1` and `arg2`: + # Create a base class that has `@arg1` and `@arg2`: # # ``` # class MyBaseClass # def initialize(arg1, arg2) - # @arg1 = arg1; + # @arg1 = arg1 # @arg2 = arg2 # end # end From cbfe403315c4eab4f37906137b99c195204d63fe Mon Sep 17 00:00:00 2001 From: Aiden Fox Ivey Date: Mon, 22 Sep 2025 17:21:37 -0400 Subject: [PATCH 3/8] ZJIT: Add polymorphism counters (#14608) * ZJIT: Add polymorphism counters * . * . --- zjit.rb | 3 ++- zjit/src/hir.rs | 31 ++++++++++++++++++++++++++ zjit/src/stats.rs | 55 +++++++++++++++++++++++++---------------------- 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/zjit.rb b/zjit.rb index e3d9d4c728f459..eb47a704708cae 100644 --- a/zjit.rb +++ b/zjit.rb @@ -41,7 +41,8 @@ def stats_string # Show non-exit counters print_counters_with_prefix(prefix: 'dynamic_send_type_', prompt: 'dynamic send types', buf:, stats:, limit: 20) - print_counters_with_prefix(prefix: 'send_fallback_', prompt: 'send fallback def_types', buf:, stats:, limit: 20) + print_counters_with_prefix(prefix: 'unspecialized_def_type_', prompt: 'send fallback unspecialized def_types', buf:, stats:, limit: 20) + print_counters_with_prefix(prefix: 'send_fallback_', prompt: 'dynamic send types', buf:, stats:, limit: 20) # Show exit counters, ordered by the typical amount of exits for the prefix at the time print_counters_with_prefix(prefix: 'unhandled_yarv_insn_', prompt: 'unhandled YARV insns', buf:, stats:, limit: 20) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 6b1d22ac111e38..b3a57a7439eea1 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1690,6 +1690,28 @@ impl Function { None } + /// Return whether a given HIR instruction as profiled by the interpreter is polymorphic or + /// whether it lacks a profile entirely. + /// + /// * `Some(true)` if polymorphic + /// * `Some(false)` if monomorphic + /// * `None` if no profiled information so far + fn is_polymorphic_at(&self, insn: InsnId, iseq_insn_idx: usize) -> Option { + let profiles = self.profiles.as_ref()?; + let entries = profiles.types.get(&iseq_insn_idx)?; + let insn = self.chase_insn(insn); + for (entry_insn, entry_type_summary) in entries { + if self.union_find.borrow().find_const(*entry_insn) == insn { + if !entry_type_summary.is_monomorphic() && !entry_type_summary.is_skewed_polymorphic() { + return Some(true); + } else { + return Some(false); + } + } + } + None + } + fn likely_is_fixnum(&self, val: InsnId, profiled_type: ProfiledType) -> bool { self.is_a(val, types::Fixnum) || profiled_type.is_fixnum() } @@ -1840,6 +1862,15 @@ impl Function { // If we know that self is reasonably monomorphic from profile information, guard and use it to fold the lookup at compile-time. // TODO(max): Figure out how to handle top self? let Some(recv_type) = self.profiled_type_of_at(recv, frame_state.insn_idx) else { + if get_option!(stats) { + match self.is_polymorphic_at(recv, frame_state.insn_idx) { + Some(true) => self.push_insn(block, Insn::IncrCounter(Counter::send_fallback_polymorphic)), + // If the class isn't known statically, then it should not also be monomorphic + Some(false) => panic!("Should not have monomorphic profile at this point in this branch"), + None => self.push_insn(block, Insn::IncrCounter(Counter::send_fallback_no_profiles)), + + }; + } self.push_insn_id(block, insn_id); continue; }; (recv_type.class(), Some(recv_type)) diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 012b8be250b43f..4f38e4a131abf8 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -138,19 +138,22 @@ make_counters! { dynamic_send_type_invokesuper, // Method call def_type related to fallback to dynamic dispatch - send_fallback_iseq, - send_fallback_cfunc, - send_fallback_attrset, - send_fallback_ivar, - send_fallback_bmethod, - send_fallback_zsuper, - send_fallback_alias, - send_fallback_undef, - send_fallback_not_implemented, - send_fallback_optimized, - send_fallback_missing, - send_fallback_refined, - send_fallback_null, + unspecialized_def_type_iseq, + unspecialized_def_type_cfunc, + unspecialized_def_type_attrset, + unspecialized_def_type_ivar, + unspecialized_def_type_bmethod, + unspecialized_def_type_zsuper, + unspecialized_def_type_alias, + unspecialized_def_type_undef, + unspecialized_def_type_not_implemented, + unspecialized_def_type_optimized, + unspecialized_def_type_missing, + unspecialized_def_type_refined, + unspecialized_def_type_null, + + send_fallback_polymorphic, + send_fallback_no_profiles, // Writes to the VM frame vm_write_pc_count, @@ -252,19 +255,19 @@ pub fn send_fallback_counter(def_type: crate::hir::MethodType) -> Counter { use crate::stats::Counter::*; match def_type { - Iseq => send_fallback_iseq, - Cfunc => send_fallback_cfunc, - Attrset => send_fallback_attrset, - Ivar => send_fallback_ivar, - Bmethod => send_fallback_bmethod, - Zsuper => send_fallback_zsuper, - Alias => send_fallback_alias, - Undefined => send_fallback_undef, - NotImplemented => send_fallback_not_implemented, - Optimized => send_fallback_optimized, - Missing => send_fallback_missing, - Refined => send_fallback_refined, - Null => send_fallback_null, + Iseq => unspecialized_def_type_iseq, + Cfunc => unspecialized_def_type_cfunc, + Attrset => unspecialized_def_type_attrset, + Ivar => unspecialized_def_type_ivar, + Bmethod => unspecialized_def_type_bmethod, + Zsuper => unspecialized_def_type_zsuper, + Alias => unspecialized_def_type_alias, + Undefined => unspecialized_def_type_undef, + NotImplemented => unspecialized_def_type_not_implemented, + Optimized => unspecialized_def_type_optimized, + Missing => unspecialized_def_type_missing, + Refined => unspecialized_def_type_refined, + Null => unspecialized_def_type_null, } } From 93c728ce9143b2f32e5d3073c8d2c87da8e0db54 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Mon, 22 Sep 2025 22:25:01 +0100 Subject: [PATCH 4/8] YJIT: Pass iseq pointer to get/set classvariable functions (#14625) * YJIT: Pass iseq pointer to get/set classvariable functions Since we already have the iseq pointer, we can actually save one memory read by passing it directly. We need to wrap the iseq in a VALUE so it can be marked correctly by GC. * YJIT: Fix missing GC marking when passing iseq to rb_vm_setinstancevariable Without wrapping the iseq in a `Operand::Value`, the iseq would not be marked correctly by GC and when compacting the heap, the iseq would be lost and cause a crash. --- yjit/src/codegen.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index 85fed25d24e48f..67841d2fdc721e 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -3148,7 +3148,7 @@ fn gen_set_ivar( asm.ccall( rb_vm_setinstancevariable as *const u8, vec![ - Opnd::const_ptr(jit.iseq as *const u8), + VALUE(jit.iseq as usize).into(), Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SELF), ivar_name.into(), val_opnd, @@ -10207,7 +10207,7 @@ fn gen_getclassvariable( let val_opnd = asm.ccall( rb_vm_getclassvariable as *const u8, vec![ - Opnd::mem(64, CFP, RUBY_OFFSET_CFP_ISEQ), + VALUE(jit.iseq as usize).into(), CFP, Opnd::UImm(jit.get_arg(0).as_u64()), Opnd::UImm(jit.get_arg(1).as_u64()), @@ -10231,7 +10231,7 @@ fn gen_setclassvariable( asm.ccall( rb_vm_setclassvariable as *const u8, vec![ - Opnd::mem(64, CFP, RUBY_OFFSET_CFP_ISEQ), + VALUE(jit.iseq as usize).into(), CFP, Opnd::UImm(jit.get_arg(0).as_u64()), val, From 6b088c876a4e2d41f39acfb0b338b6a178b30089 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 22 Sep 2025 17:27:54 -0400 Subject: [PATCH 5/8] ZJIT: Clean up RangeType (#14627) --- zjit/src/codegen.rs | 2 +- zjit/src/hir.rs | 15 +-------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 03228db0bb7c01..bd7ab847861605 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -1290,7 +1290,7 @@ fn gen_new_range( gen_prepare_non_leaf_call(jit, asm, state); // Call rb_range_new(low, high, flag) - asm_ccall!(asm, rb_range_new, low, high, (flag as i64).into()) + asm_ccall!(asm, rb_range_new, low, high, (flag as i32).into()) } fn gen_new_range_fixnum( diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index b3a57a7439eea1..0351b965f8a3ca 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -286,6 +286,7 @@ impl Const { } } +#[derive(Clone, Copy)] pub enum RangeType { Inclusive = 0, // include the end value Exclusive = 1, // exclude the end value @@ -306,14 +307,6 @@ impl std::fmt::Debug for RangeType { } } -impl Clone for RangeType { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for RangeType {} - impl From for RangeType { fn from(flag: u32) -> Self { match flag { @@ -324,12 +317,6 @@ impl From for RangeType { } } -impl From for u32 { - fn from(range_type: RangeType) -> Self { - range_type as u32 - } -} - /// Special regex backref symbol types #[derive(Debug, Clone, Copy, PartialEq)] pub enum SpecialBackrefSymbol { From 98de251c4118d13fa3d17481e48b19253f9f41e0 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 22 Sep 2025 17:54:06 -0400 Subject: [PATCH 6/8] ZJIT: Add more type information to DefinedIvar (#14628) Add test. --- zjit/src/hir.rs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 0351b965f8a3ca..faf363e32a0bc6 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1556,7 +1556,7 @@ impl Function { Insn::InvokeBlock { .. } => types::BasicObject, Insn::InvokeBuiltin { return_type, .. } => return_type.unwrap_or(types::BasicObject), Insn::Defined { pushval, .. } => Type::from_value(*pushval).union(types::NilClass), - Insn::DefinedIvar { .. } => types::BasicObject, + Insn::DefinedIvar { pushval, .. } => Type::from_value(*pushval).union(types::NilClass), Insn::GetConstantPath { .. } => types::BasicObject, Insn::ArrayMax { .. } => types::BasicObject, Insn::GetGlobal { .. } => types::BasicObject, @@ -4989,12 +4989,41 @@ mod tests { assert_snapshot!(hir_string("test"), @r" fn test@:2: bb0(v0:BasicObject): - v5:BasicObject = DefinedIvar v0, :@foo + v5:StringExact|NilClass = DefinedIvar v0, :@foo CheckInterrupts Return v5 "); } + #[test] + fn if_defined_ivar() { + eval(" + def test + if defined?(@foo) + 3 + else + 4 + end + end + "); + assert_contains_opcode("test", YARVINSN_definedivar); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(v0:BasicObject): + v5:TrueClass|NilClass = DefinedIvar v0, :@foo + CheckInterrupts + v8:CBool = Test v5 + IfFalse v8, bb1(v0) + v12:Fixnum[3] = Const Value(3) + CheckInterrupts + Return v12 + bb1(v18:BasicObject): + v22:Fixnum[4] = Const Value(4) + CheckInterrupts + Return v22 + "); + } + #[test] fn defined() { eval(" From 1d00c1114aa36de682483784e3b6b94e3c71b8f7 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Mon, 22 Sep 2025 15:55:05 -0700 Subject: [PATCH 7/8] ZJIT: Clarify --zjit-disable-hir-opt job names (#14632) --- .github/workflows/zjit-macos.yml | 4 ++-- .github/workflows/zjit-ubuntu.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 4262cc87b9cfec..bc4fef7ce2427f 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -39,8 +39,8 @@ jobs: test_all_opts: '--seed=46450' - test_task: 'check' - run_opts: '--zjit-call-threshold=1 --zjit-disable-hir-opt' - specopts: '-T --zjit-call-threshold=1 -T --zjit-disable-hir-opt' + run_opts: '--zjit-disable-hir-opt --zjit-call-threshold=1' + specopts: '-T --zjit-disable-hir-opt -T --zjit-call-threshold=1' configure: '--enable-zjit=dev' test_all_opts: '--seed=46450' diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 5dbe9ec52449c8..813f88c53b6713 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -58,8 +58,8 @@ jobs: test_all_opts: '--seed=39471' - test_task: 'check' - run_opts: '--zjit-call-threshold=1 --zjit-disable-hir-opt' - specopts: '-T --zjit-call-threshold=1 -T --zjit-disable-hir-opt' + run_opts: '--zjit-disable-hir-opt --zjit-call-threshold=1' + specopts: '-T --zjit-disable-hir-opt -T --zjit-call-threshold=1' configure: '--enable-zjit=dev' test_all_opts: '--seed=39471' From cdb9c2543415588c485282e460cdaba09452ab6a Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Mon, 22 Sep 2025 15:57:57 -0700 Subject: [PATCH 8/8] ZJIT: Guard receiver class for CCallVariadic (#14630) --- test/ruby/test_zjit.rb | 7 +++++++ zjit/src/hir.rs | 13 ++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index f0109bda8b147c..c1e8cfa8059f08 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -470,6 +470,13 @@ def entry = test(a: 1, b: 2) } end + def test_send_ccall_variadic_with_different_receiver_classes + assert_compiles '[true, true]', %q{ + def test(obj) = obj.start_with?("a") + [test("abc"), test(:abc)] + }, call_threshold: 2 + end + def test_forwardable_iseq assert_compiles '1', %q{ def test(...) = 1 diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index faf363e32a0bc6..136764f137b99b 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2169,6 +2169,11 @@ impl Function { state }); + if let Some(profiled_type) = profiled_type { + // Guard receiver class + recv = fun.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); + } + let cfun = unsafe { get_mct_func(cfunc) }.cast(); let ccall = fun.push_insn(block, Insn::CCallVariadic { cfun, @@ -7131,9 +7136,10 @@ mod opt_tests { v4:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v6:StringExact = StringCopy v4 PatchPoint MethodRedefined(Object@0x1008, puts@0x1010, cme:0x1018) - v16:BasicObject = CCallVariadic puts@0x1040, v0, v6 + v16:HeapObject[class_exact*:Object@VALUE(0x1008)] = GuardType v0, HeapObject[class_exact*:Object@VALUE(0x1008)] + v17:BasicObject = CCallVariadic puts@0x1040, v16, v6 CheckInterrupts - Return v16 + Return v17 "); } @@ -8515,7 +8521,8 @@ mod opt_tests { PatchPoint MethodRedefined(Set@0x1008, new@0x1010, cme:0x1018) v10:HeapObject = ObjectAlloc v34 PatchPoint MethodRedefined(Set@0x1008, initialize@0x1040, cme:0x1048) - v39:BasicObject = CCallVariadic initialize@0x1070, v10 + v39:HeapObject[class_exact:Set] = GuardType v10, HeapObject[class_exact:Set] + v40:BasicObject = CCallVariadic initialize@0x1070, v39 CheckInterrupts CheckInterrupts Return v10