Skip to content

Commit 714b4b5

Browse files
authored
ZJIT: Compile modified path in getblockparamproxy (#16573)
ZJIT: Avoid side exits on modified getblockparamproxy
1 parent 258c6b0 commit 714b4b5

File tree

5 files changed

+563
-155
lines changed

5 files changed

+563
-155
lines changed

zjit/src/codegen_tests.rs

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,33 @@ fn test_getblockparamproxy() {
420420
assert_snapshot!(assert_compiles("test { 1 }"), @"1");
421421
}
422422

423+
#[test]
424+
fn test_getblockparamproxy_modified() {
425+
eval("
426+
def test(&block)
427+
b = block
428+
0.then(&block)
429+
end
430+
test { 1 }
431+
");
432+
assert_contains_opcode("test", YARVINSN_getblockparamproxy);
433+
assert_snapshot!(inspect("test { 1 }"), @"1");
434+
}
435+
436+
#[test]
437+
fn test_getblockparamproxy_modified_nested_block() {
438+
eval("
439+
def test(&block)
440+
proc do
441+
b = block
442+
0.then(&block)
443+
end
444+
end
445+
test { 1 }.call
446+
");
447+
assert_snapshot!(inspect("test { 1 }.call"), @"1");
448+
}
449+
423450
#[test]
424451
fn test_getblockparam() {
425452
eval("
@@ -462,9 +489,7 @@ fn test_setblockparam_nested_block() {
462489
}
463490

464491
#[test]
465-
fn test_setblockparam_side_exit() {
466-
// This pattern side exits because `block.call` goes through
467-
// getblockparamproxy's modified-block-parameter case.
492+
fn test_getblockparamproxy_after_setblockparam() {
468493
eval("
469494
def test(&block)
470495
block = proc { 3 }
@@ -473,21 +498,7 @@ fn test_setblockparam_side_exit() {
473498
test { 1 }
474499
");
475500
assert_contains_opcode("test", YARVINSN_setblockparam);
476-
assert_snapshot!(inspect("test { 1 }"), @"3");
477-
}
478-
479-
#[test]
480-
fn test_getblockparam_proxy_side_exit_restores_block_local() {
481-
eval("
482-
def test(&block)
483-
b = block
484-
raise \"test\" unless block
485-
b ? 2 : 3
486-
end
487-
test {}
488-
");
489-
assert_contains_opcode("test", YARVINSN_getblockparam);
490-
assert_snapshot!(assert_compiles("test {}"), @"2");
501+
assert_snapshot!(assert_compiles("test { 1 }"), @"3");
491502
}
492503

493504
#[test]

zjit/src/hir.rs

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,6 @@ pub enum SideExitReason {
524524
CalleeSideExit,
525525
ObjToStringFallback,
526526
Interrupt,
527-
BlockParamProxyModified,
528527
BlockParamProxyNotIseqOrIfunc,
529528
BlockParamProxyNotNil,
530529
BlockParamWbRequired,
@@ -2611,6 +2610,24 @@ impl Function {
26112610
id
26122611
}
26132612

2613+
fn new_branch_block(
2614+
&mut self,
2615+
insn_idx: u32,
2616+
exit_state: &FrameState,
2617+
locals_count: usize,
2618+
stack_count: usize,
2619+
) -> (BlockId, InsnId, FrameState, InsnId) {
2620+
let block = self.new_block(insn_idx);
2621+
let self_param = self.push_insn(block, Insn::Param);
2622+
let mut state = exit_state.clone();
2623+
state.locals.clear();
2624+
state.stack.clear();
2625+
state.locals.extend((0..locals_count).map(|_| self.push_insn(block, Insn::Param)));
2626+
state.stack.extend((0..stack_count).map(|_| self.push_insn(block, Insn::Param)));
2627+
let snapshot = self.push_insn(block, Insn::Snapshot { state: state.clone() });
2628+
(block, self_param, state, snapshot)
2629+
}
2630+
26142631
fn remove_block(&mut self, block_id: BlockId) {
26152632
if BlockId(self.blocks.len() - 1) != block_id {
26162633
panic!("Can only remove the last block");
@@ -7578,7 +7595,9 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
75787595
});
75797596
}
75807597
YARVINSN_getblockparamproxy => {
7598+
let ep_offset = get_arg(pc, 0).as_u32();
75817599
let level = get_arg(pc, 1).as_u32();
7600+
let branch_insn_idx = exit_state.insn_idx as u32;
75827601

75837602
let profiled_block_type = if let Some([block_handler_distribution]) = profiles.payload.profile.get_operand_types(exit_state.insn_idx) {
75847603
let summary = TypeDistributionSummary::new(block_handler_distribution);
@@ -7587,16 +7606,44 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
75877606
None
75887607
};
75897608

7609+
let locals_count = state.locals.len();
7610+
let stack_count = state.stack.len();
7611+
let entry_args = state.as_args(self_param);
7612+
7613+
// `getblockparamproxy` has two semantic paths:
7614+
// - modified: return the already-materialized block local from EP
7615+
// - unmodified: inspect the block handler and produce proxy/nil
7616+
let (modified_block, modified_self_param, mut modified_state, ..) =
7617+
fun.new_branch_block(branch_insn_idx, &exit_state, locals_count, stack_count);
7618+
let (unmodified_block, unmodified_self_param, mut unmodified_state, unmodified_exit_id) =
7619+
fun.new_branch_block(branch_insn_idx, &exit_state, locals_count, stack_count);
7620+
let join_block = insn_idx_to_block.get(&insn_idx).copied().unwrap_or_else(|| fun.new_block(insn_idx));
7621+
75907622
let ep = fun.push_insn(block, Insn::GetEP { level });
7591-
let flags = fun.push_insn(block, Insn::LoadField { recv: ep, id: ID!(_env_data_index_flags), offset: SIZEOF_VALUE_I32 * (VM_ENV_DATA_INDEX_FLAGS as i32), return_type: types::CInt64 });
7592-
fun.push_insn(block, Insn::GuardNoBitsSet { val: flags, mask: Const::CUInt64(VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM.into()), mask_name: Some(ID!(VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM)), reason: SideExitReason::BlockParamProxyModified, state: exit_id });
7623+
let is_modified = fun.push_insn(block, Insn::IsBlockParamModified { ep });
7624+
7625+
fun.push_insn(block, Insn::IfTrue { val: is_modified, target: BranchEdge { target: modified_block, args: entry_args.clone() }});
7626+
fun.push_insn(block, Insn::Jump(BranchEdge { target: unmodified_block, args: entry_args }));
7627+
7628+
// Push modified block: load the block local via EP.
7629+
let ep = fun.push_insn(modified_block, Insn::GetEP { level });
7630+
let modified_val = fun.get_local_from_ep(modified_block, ep, ep_offset, level, types::BasicObject);
7631+
if level == 0 {
7632+
modified_state.setlocal(ep_offset, modified_val);
7633+
}
7634+
modified_state.stack_push(modified_val);
7635+
fun.push_insn(modified_block, Insn::Jump(BranchEdge { target: join_block, args: modified_state.as_args(modified_self_param) }));
75937636

7594-
let block_handler = fun.push_insn(block, Insn::LoadField { recv: ep, id: ID!(_env_data_index_specval), offset: SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL, return_type: types::CInt64 });
7637+
// Push unmodified block: inspect the current block handler to
7638+
// decide whether this path returns `nil` or `BlockParamProxy`.
7639+
let ep = fun.push_insn(unmodified_block, Insn::GetEP { level });
7640+
let block_handler = fun.push_insn(unmodified_block, Insn::LoadField { recv: ep, id: ID!(_env_data_index_specval), offset: SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL, return_type: types::CInt64 });
75957641

75967642
match profiled_block_type {
75977643
Some(ty) if ty.nil_p() => {
7598-
fun.push_insn(block, Insn::GuardBitEquals { val: block_handler, expected: Const::CInt64(VM_BLOCK_HANDLER_NONE.into()), reason: SideExitReason::BlockParamProxyNotNil, state: exit_id });
7599-
state.stack_push(fun.push_insn(block, Insn::Const { val: Const::Value(Qnil) }));
7644+
fun.push_insn(unmodified_block, Insn::GuardBitEquals { val: block_handler, expected: Const::CInt64(VM_BLOCK_HANDLER_NONE.into()), reason: SideExitReason::BlockParamProxyNotNil, state: unmodified_exit_id });
7645+
unmodified_state.stack_push(fun.push_insn(unmodified_block, Insn::Const { val: Const::Value(Qnil) }));
7646+
fun.push_insn(unmodified_block, Insn::Jump(BranchEdge { target: join_block, args: unmodified_state.as_args(unmodified_self_param) }));
76007647
}
76017648
_ => {
76027649
// This handles two cases which are nearly identical
@@ -7607,31 +7654,19 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
76077654
const _: () = assert!(RUBY_SYMBOL_FLAG & 1 == 0, "guard below rejects symbol block handlers");
76087655

76097656
// Bail out if the block handler is neither ISEQ nor ifunc
7610-
fun.push_insn(block, Insn::GuardAnyBitSet { val: block_handler, mask: Const::CUInt64(0x1), mask_name: None, reason: SideExitReason::BlockParamProxyNotIseqOrIfunc, state: exit_id });
7657+
fun.push_insn(unmodified_block, Insn::GuardAnyBitSet { val: block_handler, mask: Const::CUInt64(0x1), mask_name: None, reason: SideExitReason::BlockParamProxyNotIseqOrIfunc, state: unmodified_exit_id });
76117658
// TODO(Shopify/ruby#753): GC root, so we should be able to avoid unnecessary GC tracing
7612-
state.stack_push(fun.push_insn(block, Insn::Const { val: Const::Value(unsafe { rb_block_param_proxy }) }));
7659+
unmodified_state.stack_push(fun.push_insn(unmodified_block, Insn::Const { val: Const::Value(unsafe { rb_block_param_proxy }) }));
7660+
fun.push_insn(unmodified_block, Insn::Jump(BranchEdge { target: join_block, args: unmodified_state.as_args(unmodified_self_param) }));
76137661
}
76147662
}
7663+
7664+
// Continue compilation from the merged continuation block at the next
7665+
// instruction.
7666+
queue.push_back((unmodified_state, join_block, insn_idx, local_inval));
7667+
break;
76157668
}
76167669
YARVINSN_getblockparam => {
7617-
fn new_branch_block(
7618-
fun: &mut Function,
7619-
insn_idx: u32,
7620-
exit_state: &FrameState,
7621-
locals_count: usize,
7622-
stack_count: usize,
7623-
) -> (BlockId, InsnId, FrameState, InsnId) {
7624-
let block = fun.new_block(insn_idx);
7625-
let self_param = fun.push_insn(block, Insn::Param);
7626-
let mut state = exit_state.clone();
7627-
state.locals.clear();
7628-
state.stack.clear();
7629-
state.locals.extend((0..locals_count).map(|_| fun.push_insn(block, Insn::Param)));
7630-
state.stack.extend((0..stack_count).map(|_| fun.push_insn(block, Insn::Param)));
7631-
let snapshot = fun.push_insn(block, Insn::Snapshot { state: state.clone() });
7632-
(block, self_param, state, snapshot)
7633-
}
7634-
76357670
fn finish_getblockparam_branch(
76367671
fun: &mut Function,
76377672
block: BlockId,
@@ -7667,9 +7702,9 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
76677702

76687703
// Set up branch and join blocks.
76697704
let (modified_block, modified_self_param, mut modified_state, ..) =
7670-
new_branch_block(&mut fun, branch_insn_idx, &exit_state, locals_count, stack_count);
7705+
fun.new_branch_block(branch_insn_idx, &exit_state, locals_count, stack_count);
76717706
let (unmodified_block, unmodified_self_param, mut unmodified_state, unmodified_exit_id) =
7672-
new_branch_block(&mut fun, branch_insn_idx, &exit_state, locals_count, stack_count);
7707+
fun.new_branch_block(branch_insn_idx, &exit_state, locals_count, stack_count);
76737708
let join_block = insn_idx_to_block.get(&insn_idx).copied().unwrap_or_else(|| fun.new_block(insn_idx));
76747709

76757710
fun.push_insn(block, Insn::IfTrue {

0 commit comments

Comments
 (0)