summaryrefslogtreecommitdiff
path: root/zjit
diff options
context:
space:
mode:
authorTakashi Kokubun <takashikkbn@gmail.com>2026-03-26 22:38:27 -0700
committerTakashi Kokubun <takashikkbn@gmail.com>2026-03-27 09:51:39 -0700
commit06f746fd8abd2ced4b3159171c52614b90ef37c2 (patch)
treeed8d126ecfb37aaab095ebd71cc9fd6c397d7268 /zjit
parent7b85b2143273f437d475e19d87f3cbb5c95842ea (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.rs15
-rw-r--r--zjit/src/hir/opt_tests.rs74
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