From 59f55f81dc16187de9270625b1d38e59cd9a6808 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Wed, 10 Dec 2025 17:38:03 -0500 Subject: [PATCH] ZJIT: Eliminate GuardType on frozen objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When GuardType operates on a value that is a known frozen object (from a constant), we can check at compile time if the object's type matches the guard. Since frozen objects cannot change their class, the runtime guard becomes unnecessary. This optimization eliminates redundant type guards when accessing methods on frozen constant objects, complementing the LoadField folding for ivar reads. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- zjit/src/hir.rs | 20 ++++-- zjit/src/hir/opt_tests.rs | 134 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 4 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index fbc9d80700dcaf..4a7f45470a3fce 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -3362,10 +3362,22 @@ impl Function { let mut new_insns = vec![]; for insn_id in old_insns { let replacement_id = match self.find(insn_id) { - Insn::GuardType { val, guard_type, .. } if self.is_a(val, guard_type) => { - self.make_equal_to(insn_id, val); - // Don't bother re-inferring the type of val; we already know it. - continue; + Insn::GuardType { val, guard_type, .. } => { + // If we already know the type matches, eliminate the guard + if self.is_a(val, guard_type) { + self.make_equal_to(insn_id, val); + continue; + } + // If GuardType is on a frozen object, we can check at compile time if the + // object's type matches the guard. Frozen objects can't change class. + let val_type = self.type_of(val); + if let Some(obj) = val_type.ruby_object() { + if obj.is_frozen() && Type::from_value(obj).is_subtype(guard_type) { + self.make_equal_to(insn_id, val); + continue; + } + } + insn_id } Insn::FixnumAdd { left, right, .. } => { self.fold_fixnum_bop(insn_id, left, right, |l, r| match (l, r) { diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index b32da5a9ebb8e5..6226ea0b0db092 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -8633,4 +8633,138 @@ mod hir_opt_tests { Return v60 "); } + + #[test] + fn test_eliminate_guard_type_on_frozen_object() { + // When a frozen constant object matches the guard type, eliminate the GuardType + eval(r#" + class C + def foo = 42 + end + FROZEN = C.new.freeze + def test = FROZEN.foo + test; test + "#); + // The GuardType should be eliminated because FROZEN is a frozen C object + // and the guard is for HeapObject[class_exact:C] + assert_snapshot!(hir_string("test"), @r" + fn test@:6: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, FROZEN) + v20:HeapObject[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(C@0x1010, foo@0x1018, cme:0x1020) + PatchPoint NoSingletonClass(C@0x1010) + IncrCounter inline_iseq_optimized_send_count + v25:Fixnum[42] = Const Value(42) + CheckInterrupts + Return v25 + "); + } + + #[test] + fn test_eliminate_guard_type_on_unfrozen_constant_object() { + // Even unfrozen constant objects have their GuardType eliminated because + // the type system already knows the exact class from the constant VALUE. + // The frozen object optimization is defense-in-depth for edge cases. + eval(r#" + class C + def foo = 42 + end + UNFROZEN = C.new # Not frozen, but class is still known + def test = UNFROZEN.foo + test; test + "#); + // The GuardType is eliminated because the constant's type is fully known + assert_snapshot!(hir_string("test"), @r" + fn test@:6: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, UNFROZEN) + v20:HeapObject[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(C@0x1010, foo@0x1018, cme:0x1020) + PatchPoint NoSingletonClass(C@0x1010) + IncrCounter inline_iseq_optimized_send_count + v25:Fixnum[42] = Const Value(42) + CheckInterrupts + Return v25 + "); + } + + #[test] + fn test_eliminate_guard_type_on_frozen_string_constant() { + // Frozen string literals should also benefit from this optimization + eval(r#" + # frozen_string_literal: true + STR = "hello" + def test = STR.size + test; test + "#); + // The GuardType for StringExact should be eliminated + assert_snapshot!(hir_string("test"), @r" + fn test@:4: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, STR) + v21:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(String@0x1010, size@0x1018, cme:0x1020) + PatchPoint NoSingletonClass(String@0x1010) + IncrCounter inline_cfunc_optimized_send_count + v26:Fixnum = CCall String#size@0x1048, v21 + CheckInterrupts + Return v26 + "); + } + + #[test] + fn test_eliminate_guard_type_on_frozen_array_constant() { + // Frozen arrays should also benefit from this optimization + eval(r#" + ARR = [1, 2, 3].freeze + def test = ARR.size + test; test + "#); + // The GuardType for ArrayExact should be eliminated + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, ARR) + v21:ArrayExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(Array@0x1010, size@0x1018, cme:0x1020) + PatchPoint NoSingletonClass(Array@0x1010) + IncrCounter inline_cfunc_optimized_send_count + v26:Fixnum = CCall Array#size@0x1048, v21 + CheckInterrupts + Return v26 + "); + } }