diff options
| author | Takashi Kokubun <takashikkbn@gmail.com> | 2026-03-26 22:38:27 -0700 |
|---|---|---|
| committer | Takashi Kokubun <takashikkbn@gmail.com> | 2026-03-27 09:51:39 -0700 |
| commit | 06f746fd8abd2ced4b3159171c52614b90ef37c2 (patch) | |
| tree | ed8d126ecfb37aaab095ebd71cc9fd6c397d7268 /zjit | |
| parent | 7b85b2143273f437d475e19d87f3cbb5c95842ea (diff) | |
ZJIT: Skip convert_no_profile_sends on the final ISEQ version
When an ISEQ has already reached MAX_ISEQ_VERSIONS, converting
no-profile sends to SideExits is counterproductive: the exit fires
every time but can never trigger recompilation. Keep them as Send
fallbacks so the interpreter handles them directly without the
overhead of a SideExit + no_profile_send_recompile call.
Diffstat (limited to 'zjit')
| -rw-r--r-- | zjit/src/hir.rs | 15 | ||||
| -rw-r--r-- | zjit/src/hir/opt_tests.rs | 74 |
2 files changed, 36 insertions, 53 deletions
diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 9227bbd730..b5c5fffaa7 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2372,6 +2372,10 @@ pub struct Function { /// Whether previously, a function for this ISEQ was invalidated due to /// singleton class creation (violation of NoSingletonClass invariant). was_invalidated_for_singleton_class_creation: bool, + /// Whether this is the last allowed version for this ISEQ (at MAX_ISEQ_VERSIONS). + /// When true, convert_no_profile_sends skips converting sends to SideExits since + /// no further recompilation is possible. + is_final_version: bool, /// The types for the parameters of this function. They are copied to the type /// of entry block params after infer_types() fills Empty to all insn_types. param_types: Vec<Type>, @@ -2479,6 +2483,7 @@ impl Function { Function { iseq, was_invalidated_for_singleton_class_creation: false, + is_final_version: false, insns: vec![], insn_types: vec![], union_find: UnionFind::new().into(), @@ -5031,6 +5036,12 @@ impl Function { /// The remaining no-profile sends are turned into side exits that trigger recompilation with /// fresh profile data. fn convert_no_profile_sends(&mut self) { + // On the final version, recompilation is not possible, so converting sends to + // SideExits would just add overhead (the exit fires every time without benefit). + // Keep them as Send fallbacks so the interpreter handles them directly. + if self.is_final_version { + return; + } for block in self.rpo() { let old_insns = std::mem::take(&mut self.blocks[block.0].insns); assert!(self.blocks[block.0].insns.is_empty()); @@ -6816,6 +6827,10 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> { let mut profiles = ProfileOracle::new(payload); let mut fun = Function::new(iseq); fun.was_invalidated_for_singleton_class_creation = payload.was_invalidated_for_singleton_class_creation; + // invalidate_iseq_version only invalidates when versions.len() < MAX_ISEQ_VERSIONS. + // After this compilation, versions.len() will be current + 1. If that reaches the limit, + // exit profiling can't trigger another recompile, so SideExits would fire permanently. + fun.is_final_version = payload.versions.len() + 1 >= crate::codegen::MAX_ISEQ_VERSIONS; // Compute a map of PC->Block by finding jump targets let jit_entry_insns = jit_entry_insns(iseq); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index c7b2caf31a..96882445ed 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -14962,62 +14962,32 @@ mod hir_opt_tests { } #[test] - fn test_recompile_no_profile_send() { - // Define a callee method and a test method that calls it + fn test_no_profile_send_on_final_version() { + // On the final ISEQ version (MAX_ISEQ_VERSIONS reached), no-profile sends should + // remain as Send fallbacks instead of being converted to SideExits, since recompilation + // is no longer possible and SideExits would fire every time without benefit. + // + // Use call_threshold=3 to ensure the method is auto-compiled before hir_string() builds + // the HIR. The auto-compile creates version 1, and hir_string() creates version 2 + // (= MAX_ISEQ_VERSIONS), so is_final_version is true. + set_call_threshold(3); eval(" - def greet_recompile(x) = x.to_s - def test_no_profile_recompile(flag) + def greet_final(x) = x.to_s + def test_final_version(flag) if flag - greet_recompile(42) + greet_final(42) else 'hello' end end "); + // Call enough times to trigger auto-compilation. flag=false so greet_final is never + // reached and has no profile data. + eval("3.times { test_final_version(false) }"); - // With call_threshold=2, num_profiles=1: - // 1st call profiles (flag=false, so greet is never reached) - // 2nd call compiles (greet has no profile data -> SideExit recompile) - eval("test_no_profile_recompile(false); test_no_profile_recompile(false)"); - - // The first compilation should have SideExit NoProfileSend recompile - // for the greet_recompile(42) callsite since it was never profiled. - assert_snapshot!(hir_string("test_no_profile_recompile"), @r" - fn test_no_profile_recompile@<compiled>:4: - bb1(): - EntryPoint interpreter - v1:BasicObject = LoadSelf - v2:CPtr = LoadSP - v3:BasicObject = LoadField v2, :flag@0x1000 - Jump bb3(v1, v3) - bb2(): - EntryPoint JIT(0) - v6:BasicObject = LoadArg :self@0 - v7:BasicObject = LoadArg :flag@1 - Jump bb3(v6, v7) - bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - IfFalse v16, bb4(v9, v17) - v19:Truthy = RefineType v10, Truthy - v23:Fixnum[42] = Const Value(42) - SideExit NoProfileSend recompile - bb4(v30:BasicObject, v31:Falsy): - v35:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v36:StringExact = StringCopy v35 - CheckInterrupts - Return v36 - "); - - // Now call with flag=true. This hits the SideExit, which profiles - // the send and invalidates the ISEQ for recompilation. - eval("test_no_profile_recompile(true)"); - - // After profiling via the side exit, rebuilding HIR should now - // have a SendDirect for greet_recompile instead of SideExit. - assert_snapshot!(hir_string("test_no_profile_recompile"), @r" - fn test_no_profile_recompile@<compiled>:4: + // On the final version, greet_final should be a Send fallback, not a SideExit. + assert_snapshot!(hir_string("test_final_version"), @r" + fn test_final_version@<compiled>:4: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf @@ -15036,13 +15006,11 @@ mod hir_opt_tests { IfFalse v16, bb4(v9, v17) v19:Truthy = RefineType v10, Truthy v23:Fixnum[42] = Const Value(42) - PatchPoint MethodRedefined(Object@0x1008, greet_recompile@0x1010, cme:0x1018) - v43:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v9, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] - v44:BasicObject = SendDirect v43, 0x1040, :greet_recompile (0x1050), v23 + v25:BasicObject = Send v9, :greet_final, v23 # SendFallbackReason: SendWithoutBlock: no profile data available CheckInterrupts - Return v44 + Return v25 bb4(v30:BasicObject, v31:Falsy): - v35:StringExact[VALUE(0x1058)] = Const Value(VALUE(0x1058)) + v35:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v36:StringExact = StringCopy v35 CheckInterrupts Return v36 |
