Skip to content

Commit ebcfbb8

Browse files
committed
ZJIT: Drop has_blockiseq guard on local variable access
So `has_blockiseq` was masking two other bugs. First, that we had only a static EP escape check for an iseq. We need to check both that an iseq is not known to statically escape them *and that it has not escaped them before*. Only then can we use SSA locals. This popped up in a weird PP unit test that was reducible to the test cases added in the PR. So I fixed that. Second, that we didn't VM lock when code patching for EP escape. That showed up in the Ractor btests in my previous PRs ruby#14600 and ruby#15254 ruby#15316, and I had no idea what was going on at the time, so I dropped it. But if we add this lock, like all the other invariant patching cases have, the problem goes away. Seems reasonable. Finally, we can use normal full SSA locals even in the presence of a blockiseq. We still reload all of them after each send-with-block, but we can in a later PR reduce that to just the locals that are (recursively) visibly written to.
1 parent 3d00e34 commit ebcfbb8

File tree

3 files changed

+128
-40
lines changed

3 files changed

+128
-40
lines changed

zjit/src/hir.rs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6087,14 +6087,12 @@ pub fn jit_entry_insns(iseq: IseqPtr) -> Vec<u32> {
60876087

60886088
struct BytecodeInfo {
60896089
jump_targets: Vec<u32>,
6090-
has_blockiseq: bool,
60916090
}
60926091

60936092
fn compute_bytecode_info(iseq: *const rb_iseq_t, opt_table: &[u32]) -> BytecodeInfo {
60946093
let iseq_size = unsafe { get_iseq_encoded_size(iseq) };
60956094
let mut insn_idx = 0;
60966095
let mut jump_targets: HashSet<u32> = opt_table.iter().copied().collect();
6097-
let mut has_blockiseq = false;
60986096
while insn_idx < iseq_size {
60996097
// Get the current pc and opcode
61006098
let pc = unsafe { rb_iseq_pc_at_idx(iseq, insn_idx) };
@@ -6118,18 +6116,12 @@ fn compute_bytecode_info(iseq: *const rb_iseq_t, opt_table: &[u32]) -> BytecodeI
61186116
jump_targets.insert(insn_idx);
61196117
}
61206118
}
6121-
YARVINSN_send | YARVINSN_invokesuper => {
6122-
let blockiseq: IseqPtr = get_arg(pc, 1).as_iseq();
6123-
if !blockiseq.is_null() {
6124-
has_blockiseq = true;
6125-
}
6126-
}
61276119
_ => {}
61286120
}
61296121
}
61306122
let mut result = jump_targets.into_iter().collect::<Vec<_>>();
61316123
result.sort();
6132-
BytecodeInfo { jump_targets: result, has_blockiseq }
6124+
BytecodeInfo { jump_targets: result }
61336125
}
61346126

61356127
#[derive(Debug, PartialEq, Clone, Copy)]
@@ -6244,7 +6236,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
62446236

62456237
// Compute a map of PC->Block by finding jump targets
62466238
let jit_entry_insns = jit_entry_insns(iseq);
6247-
let BytecodeInfo { jump_targets, has_blockiseq } = compute_bytecode_info(iseq, &jit_entry_insns);
6239+
let BytecodeInfo { jump_targets } = compute_bytecode_info(iseq, &jit_entry_insns);
62486240

62496241
// Make all empty basic blocks. The ordering of the BBs matters for getting fallthrough jumps
62506242
// in good places, but it's not necessary for correctness. TODO: Higher quality scheduling during lowering.
@@ -6276,7 +6268,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
62766268

62776269
// Check if the EP is escaped for the ISEQ from the beginning. We give up
62786270
// optimizing locals in that case because they're shared with other frames.
6279-
let ep_escaped = iseq_escapes_ep(iseq);
6271+
let ep_starts_escaped = iseq_escapes_ep(iseq);
6272+
// Check if the EP has been escaped at some point in the ISEQ. If it has, then we assume that
6273+
// its EP is shared with other frames.
6274+
let ep_has_been_escaped = crate::invariants::iseq_escapes_ep(iseq);
6275+
let ep_escaped = ep_starts_escaped || ep_has_been_escaped;
62806276

62816277
// Iteratively fill out basic blocks using a queue.
62826278
// TODO(max): Basic block arguments at edges
@@ -6620,7 +6616,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
66206616
// Use FrameState to get kw_bits when possible, just like getlocal_WC_0.
66216617
let val = if !local_inval {
66226618
state.getlocal(ep_offset)
6623-
} else if ep_escaped || has_blockiseq {
6619+
} else if ep_escaped {
66246620
fun.push_insn(block, Insn::GetLocal { ep_offset, level: 0, use_sp: false, rest_param: false })
66256621
} else {
66266622
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state.without_locals() });
@@ -6743,7 +6739,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
67436739
// In case of JIT-to-JIT send locals might never end up in EP memory.
67446740
let val = state.getlocal(ep_offset);
67456741
state.stack_push(val);
6746-
} else if ep_escaped || has_blockiseq { // TODO: figure out how to drop has_blockiseq here
6742+
} else if ep_escaped {
67476743
// Read the local using EP
67486744
let val = fun.push_insn(block, Insn::GetLocal { ep_offset, level: 0, use_sp: false, rest_param: false });
67496745
state.setlocal(ep_offset, val); // remember the result to spill on side-exits
@@ -6764,7 +6760,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
67646760
YARVINSN_setlocal_WC_0 => {
67656761
let ep_offset = get_arg(pc, 0).as_u32();
67666762
let val = state.stack_pop()?;
6767-
if ep_escaped || has_blockiseq { // TODO: figure out how to drop has_blockiseq here
6763+
if ep_escaped {
67686764
// Write the local using EP
67696765
fun.push_insn(block, Insn::SetLocal { val, ep_offset, level: 0 });
67706766
} else if local_inval {

zjit/src/hir/opt_tests.rs

Lines changed: 105 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2926,15 +2926,14 @@ mod hir_opt_tests {
29262926
Jump bb2(v5, v6)
29272927
bb2(v8:BasicObject, v9:NilClass):
29282928
v13:Fixnum[1] = Const Value(1)
2929-
SetLocal :a, l0, EP@3, v13
29302929
PatchPoint NoSingletonClass(Object@0x1000)
29312930
PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010)
29322931
v31:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v8, HeapObject[class_exact*:Object@VALUE(0x1000)]
29332932
IncrCounter inline_iseq_optimized_send_count
2934-
v20:BasicObject = GetLocal :a, l0, EP@3
2935-
v24:BasicObject = GetLocal :a, l0, EP@3
2933+
v19:BasicObject = GetLocal :a, l0, EP@3
2934+
PatchPoint NoEPEscape(test)
29362935
CheckInterrupts
2937-
Return v24
2936+
Return v19
29382937
");
29392938
}
29402939

@@ -3415,15 +3414,14 @@ mod hir_opt_tests {
34153414
Jump bb2(v6, v7, v8)
34163415
bb2(v10:BasicObject, v11:BasicObject, v12:NilClass):
34173416
v16:ArrayExact = NewArray
3418-
SetLocal :a, l0, EP@3, v16
3419-
v22:TrueClass = Const Value(true)
3417+
v21:TrueClass = Const Value(true)
34203418
IncrCounter complex_arg_pass_caller_kwarg
3421-
v24:BasicObject = Send v11, 0x1000, :each_line, v22 # SendFallbackReason: Complex argument passing
3422-
v25:BasicObject = GetLocal :s, l0, EP@4
3423-
v26:BasicObject = GetLocal :a, l0, EP@3
3424-
v30:BasicObject = GetLocal :a, l0, EP@3
3419+
v23:BasicObject = Send v11, 0x1000, :each_line, v21 # SendFallbackReason: Complex argument passing
3420+
v24:BasicObject = GetLocal :s, l0, EP@4
3421+
v25:BasicObject = GetLocal :a, l0, EP@3
3422+
PatchPoint NoEPEscape(test)
34253423
CheckInterrupts
3426-
Return v30
3424+
Return v25
34273425
");
34283426
}
34293427

@@ -6523,7 +6521,6 @@ mod hir_opt_tests {
65236521
Jump bb2(v5, v6)
65246522
bb2(v8:BasicObject, v9:NilClass):
65256523
v13:ArrayExact = NewArray
6526-
SetLocal :result, l0, EP@3, v13
65276524
PatchPoint SingleRactorMode
65286525
PatchPoint StableConstantNames(0x1000, A)
65296526
v36:ArrayExact[VALUE(0x1008)] = Const Value(VALUE(0x1008))
@@ -6533,10 +6530,10 @@ mod hir_opt_tests {
65336530
PatchPoint NoSingletonClass(Array@0x1020)
65346531
PatchPoint MethodRedefined(Array@0x1020, zip@0x1028, cme:0x1030)
65356532
v43:BasicObject = CCallVariadic v36, :zip@0x1058, v39
6536-
v25:BasicObject = GetLocal :result, l0, EP@3
6537-
v29:BasicObject = GetLocal :result, l0, EP@3
6533+
v24:BasicObject = GetLocal :result, l0, EP@3
6534+
PatchPoint NoEPEscape(test)
65386535
CheckInterrupts
6539-
Return v29
6536+
Return v24
65406537
");
65416538
}
65426539

@@ -11887,4 +11884,97 @@ mod hir_opt_tests {
1188711884
Return v31
1188811885
");
1188911886
}
11887+
11888+
#[test]
11889+
fn recompile_after_ep_escape_uses_ep_locals() {
11890+
// When a method creates a lambda, EP escapes to the heap. After
11891+
// invalidation and recompilation, the compiler must use EP-based
11892+
// locals (SetLocal/GetLocal) instead of SSA locals, because the
11893+
// spill target (stack) and the read target (heap EP) diverge.
11894+
eval("
11895+
CONST = {}.freeze
11896+
def test_ep_escape(list, sep=nil, iter_method=:each)
11897+
sep ||= lambda { }
11898+
kwsplat = CONST
11899+
list.__send__(iter_method) {|*v| yield(*v) }
11900+
end
11901+
11902+
test_ep_escape({a: 1}, nil, :each_pair) { |k, v|
11903+
test_ep_escape([1], lambda { }) { |x| }
11904+
}
11905+
test_ep_escape({a: 1}, nil, :each_pair) { |k, v|
11906+
test_ep_escape([1], lambda { }) { |x| }
11907+
}
11908+
");
11909+
assert_snapshot!(hir_string("test_ep_escape"), @r"
11910+
fn test_ep_escape@<compiled>:3:
11911+
bb0():
11912+
EntryPoint interpreter
11913+
v1:BasicObject = LoadSelf
11914+
v2:BasicObject = GetLocal :list, l0, SP@7
11915+
v3:BasicObject = GetLocal :sep, l0, SP@6
11916+
v4:BasicObject = GetLocal :iter_method, l0, SP@5
11917+
v5:NilClass = Const Value(nil)
11918+
v6:CPtr = LoadPC
11919+
v7:CPtr[CPtr(0x1000)] = Const CPtr(0x1008)
11920+
v8:CBool = IsBitEqual v6, v7
11921+
IfTrue v8, bb2(v1, v2, v3, v4, v5)
11922+
v10:CPtr[CPtr(0x1000)] = Const CPtr(0x1008)
11923+
v11:CBool = IsBitEqual v6, v10
11924+
IfTrue v11, bb4(v1, v2, v3, v4, v5)
11925+
Jump bb6(v1, v2, v3, v4, v5)
11926+
bb1(v15:BasicObject, v16:BasicObject):
11927+
EntryPoint JIT(0)
11928+
v17:NilClass = Const Value(nil)
11929+
v18:NilClass = Const Value(nil)
11930+
v19:NilClass = Const Value(nil)
11931+
Jump bb2(v15, v16, v17, v18, v19)
11932+
bb2(v35:BasicObject, v36:BasicObject, v37:BasicObject, v38:BasicObject, v39:NilClass):
11933+
v42:NilClass = Const Value(nil)
11934+
SetLocal :sep, l0, EP@5, v42
11935+
Jump bb4(v35, v36, v42, v38, v39)
11936+
bb3(v22:BasicObject, v23:BasicObject, v24:BasicObject):
11937+
EntryPoint JIT(1)
11938+
v25:NilClass = Const Value(nil)
11939+
v26:NilClass = Const Value(nil)
11940+
Jump bb4(v22, v23, v24, v25, v26)
11941+
bb4(v46:BasicObject, v47:BasicObject, v48:BasicObject, v49:BasicObject, v50:NilClass):
11942+
v53:StaticSymbol[:each] = Const Value(VALUE(0x1010))
11943+
SetLocal :iter_method, l0, EP@4, v53
11944+
Jump bb6(v46, v47, v48, v53, v50)
11945+
bb5(v29:BasicObject, v30:BasicObject, v31:BasicObject, v32:BasicObject):
11946+
EntryPoint JIT(2)
11947+
v33:NilClass = Const Value(nil)
11948+
Jump bb6(v29, v30, v31, v32, v33)
11949+
bb6(v57:BasicObject, v58:BasicObject, v59:BasicObject, v60:BasicObject, v61:NilClass):
11950+
CheckInterrupts
11951+
v67:CBool = Test v59
11952+
v68:Truthy = RefineType v59, Truthy
11953+
IfTrue v67, bb7(v57, v58, v68, v60, v61)
11954+
v70:Falsy = RefineType v59, Falsy
11955+
PatchPoint NoSingletonClass(Object@0x1018)
11956+
PatchPoint MethodRedefined(Object@0x1018, lambda@0x1020, cme:0x1028)
11957+
v114:HeapObject[class_exact*:Object@VALUE(0x1018)] = GuardType v57, HeapObject[class_exact*:Object@VALUE(0x1018)]
11958+
v115:BasicObject = CCallWithFrame v114, :Kernel#lambda@0x1050, block=0x1058
11959+
v74:BasicObject = GetLocal :list, l0, EP@6
11960+
v76:BasicObject = GetLocal :iter_method, l0, EP@4
11961+
v77:BasicObject = GetLocal :kwsplat, l0, EP@3
11962+
SetLocal :sep, l0, EP@5, v115
11963+
Jump bb7(v57, v74, v115, v76, v77)
11964+
bb7(v81:BasicObject, v82:BasicObject, v83:BasicObject, v84:BasicObject, v85:BasicObject):
11965+
PatchPoint SingleRactorMode
11966+
PatchPoint StableConstantNames(0x1060, CONST)
11967+
v110:HashExact[VALUE(0x1068)] = Const Value(VALUE(0x1068))
11968+
SetLocal :kwsplat, l0, EP@3, v110
11969+
v95:BasicObject = GetLocal :list, l0, EP@6
11970+
v97:BasicObject = GetLocal :iter_method, l0, EP@4
11971+
v99:BasicObject = Send v95, 0x1070, :__send__, v97 # SendFallbackReason: Send: unsupported method type Optimized
11972+
v100:BasicObject = GetLocal :list, l0, EP@6
11973+
v101:BasicObject = GetLocal :sep, l0, EP@5
11974+
v102:BasicObject = GetLocal :iter_method, l0, EP@4
11975+
v103:BasicObject = GetLocal :kwsplat, l0, EP@3
11976+
CheckInterrupts
11977+
Return v99
11978+
");
11979+
}
1189011980
}

zjit/src/invariants.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -206,20 +206,22 @@ pub extern "C" fn rb_zjit_invalidate_no_ep_escape(iseq: IseqPtr) {
206206
return;
207207
}
208208

209-
// Remember that this ISEQ may escape EP
210-
let invariants = ZJITState::get_invariants();
211-
invariants.ep_escape_iseqs.insert(iseq);
209+
with_vm_lock(src_loc!(), || {
210+
// Remember that this ISEQ may escape EP
211+
let invariants = ZJITState::get_invariants();
212+
invariants.ep_escape_iseqs.insert(iseq);
212213

213-
// If the ISEQ has been compiled assuming it doesn't escape EP, invalidate the JIT code.
214-
if let Some(patch_points) = invariants.no_ep_escape_iseq_patch_points.get(&iseq) {
215-
debug!("EP is escaped: {}", iseq_name(iseq));
214+
// If the ISEQ has been compiled assuming it doesn't escape EP, invalidate the JIT code.
215+
if let Some(patch_points) = invariants.no_ep_escape_iseq_patch_points.get(&iseq) {
216+
debug!("EP is escaped: {}", iseq_name(iseq));
216217

217-
// Invalidate the patch points for this ISEQ
218-
let cb = ZJITState::get_code_block();
219-
compile_patch_points!(cb, patch_points, "EP is escaped: {}", iseq_name(iseq));
218+
// Invalidate the patch points for this ISEQ
219+
let cb = ZJITState::get_code_block();
220+
compile_patch_points!(cb, patch_points, "EP is escaped: {}", iseq_name(iseq));
220221

221-
cb.mark_all_executable();
222-
}
222+
cb.mark_all_executable();
223+
}
224+
});
223225
}
224226

225227
/// Track that JIT code for a ISEQ will assume that base pointer is equal to environment pointer.

0 commit comments

Comments
 (0)