Skip to content

Commit 8886990

Browse files
authored
ZJIT: Add polymorphic support for getblockparamproxy (ruby#16636)
1 parent 7209523 commit 8886990

File tree

5 files changed

+273
-21
lines changed

5 files changed

+273
-21
lines changed

zjit/src/codegen_tests.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,20 @@ fn test_getblockparamproxy_modified_nested_block() {
447447
assert_snapshot!(inspect("test { 1 }.call"), @"1");
448448
}
449449

450+
#[test]
451+
fn test_getblockparamproxy_polymorphic_none_and_iseq() {
452+
set_call_threshold(3);
453+
eval("
454+
def test(&block)
455+
0.then(&block)
456+
end
457+
test
458+
test { 1 }
459+
");
460+
assert_contains_opcode("test", YARVINSN_getblockparamproxy);
461+
assert_snapshot!(assert_compiles("test { 2 }"), @"2");
462+
}
463+
450464
#[test]
451465
fn test_getblockparam() {
452466
eval("

zjit/src/hir.rs

Lines changed: 143 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,8 @@ pub enum SideExitReason {
531531
Interrupt,
532532
BlockParamProxyNotIseqOrIfunc,
533533
BlockParamProxyNotNil,
534+
BlockParamProxyFallbackMiss,
535+
BlockParamProxyProfileNotCovered,
534536
BlockParamWbRequired,
535537
StackOverflow,
536538
FixnumModByZero,
@@ -7575,17 +7577,31 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
75757577
});
75767578
}
75777579
YARVINSN_getblockparamproxy => {
7580+
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
7581+
enum ProfiledBlockHandlerFamily {
7582+
Nil,
7583+
IseqOrIfunc,
7584+
}
7585+
impl ProfiledBlockHandlerFamily {
7586+
fn from_profiled_type(profiled_type: ProfiledType) -> Option<Self> {
7587+
let obj = profiled_type.class();
7588+
if obj.nil_p() {
7589+
Some(Self::Nil)
7590+
} else if unsafe {
7591+
rb_IMEMO_TYPE_P(obj, imemo_iseq) == 1
7592+
|| rb_IMEMO_TYPE_P(obj, imemo_ifunc) == 1
7593+
} {
7594+
Some(Self::IseqOrIfunc)
7595+
} else {
7596+
None
7597+
}
7598+
}
7599+
}
7600+
75787601
let ep_offset = get_arg(pc, 0).as_u32();
75797602
let level = get_arg(pc, 1).as_u32();
75807603
let branch_insn_idx = exit_state.insn_idx as u32;
75817604

7582-
let profiled_block_type = if let Some([block_handler_distribution]) = profiles.payload.profile.get_operand_types(exit_state.insn_idx) {
7583-
let summary = TypeDistributionSummary::new(block_handler_distribution);
7584-
summary.is_monomorphic().then_some(summary.bucket(0).class())
7585-
} else {
7586-
None
7587-
};
7588-
75897605
// `getblockparamproxy` has two semantic paths:
75907606
// - modified: return the already-materialized block local from EP
75917607
// - unmodified: inspect the block handler and produce proxy/nil
@@ -7611,30 +7627,136 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
76117627
// decide whether this path returns `nil` or `BlockParamProxy`.
76127628
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 });
76137629
let original_local = if level == 0 { Some(state.getlocal(ep_offset)) } else { None };
7630+
// `block_handler & 1 == 1` accepts both ISEQ (0b01) and ifunc
7631+
// (0b11) handlers. Keep a compile-time check that this shortcut
7632+
// does not accidentally accept symbol block handlers.
7633+
const _: () = assert!(RUBY_SYMBOL_FLAG & 1 == 0, "guard below rejects symbol block handlers");
7634+
7635+
7636+
let profiled_block_summary = profiles.payload.profile.get_operand_types(exit_state.insn_idx)
7637+
.and_then(|types| types.first())
7638+
.map(TypeDistributionSummary::new);
76147639

7615-
match profiled_block_type {
7616-
Some(ty) if ty.nil_p() => {
7617-
fun.push_insn(unmodified_block, Insn::GuardBitEquals { val: block_handler, expected: Const::CInt64(VM_BLOCK_HANDLER_NONE.into()), reason: SideExitReason::BlockParamProxyNotNil, state: exit_id, recompile: None });
7618-
let nil_val = fun.push_insn(unmodified_block, Insn::Const { val: Const::Value(Qnil) });
7619-
let mut unmodified_args = vec![nil_val];
7620-
if let Some(local) = original_local { unmodified_args.push(local); }
7621-
fun.push_insn(unmodified_block, Insn::Jump(BranchEdge { target: join_block, args: unmodified_args }));
7640+
let mut profiled_handlers = Vec::new();
7641+
if let Some(summary) = profiled_block_summary.as_ref() {
7642+
if summary.is_monomorphic() || summary.is_polymorphic() || summary.is_skewed_polymorphic() {
7643+
for &profiled_type in summary.buckets() {
7644+
if profiled_type.is_empty() {
7645+
break;
7646+
}
7647+
if let Some(profiled_handler) = ProfiledBlockHandlerFamily::from_profiled_type(profiled_type) {
7648+
if !profiled_handlers.contains(&profiled_handler) {
7649+
profiled_handlers.push(profiled_handler);
7650+
}
7651+
}
7652+
}
76227653
}
7623-
_ => {
7624-
// This handles two cases which are nearly identical
7654+
}
7655+
7656+
match profiled_handlers.as_slice() {
7657+
// No supported profiled families. Keep the generic fallback iseq/ifunc fallback
7658+
// for sites we do not specialize, such as no-profile and megamorphic sites.
7659+
[] => {
7660+
// This handles two cases which are nearly identical.
76257661
// Block handler is a tagged pointer. Look at the tag.
76267662
// VM_BH_ISEQ_BLOCK_P(): block_handler & 0x03 == 0x01
76277663
// VM_BH_IFUNC_P(): block_handler & 0x03 == 0x03
76287664
// So to check for either of those cases we can use: val & 0x1 == 0x1
7629-
const _: () = assert!(RUBY_SYMBOL_FLAG & 1 == 0, "guard below rejects symbol block handlers");
76307665

76317666
// Bail out if the block handler is neither ISEQ nor ifunc
7632-
fun.push_insn(unmodified_block, Insn::GuardAnyBitSet { val: block_handler, mask: Const::CUInt64(0x1), mask_name: None, reason: SideExitReason::BlockParamProxyNotIseqOrIfunc, state: exit_id });
7667+
fun.push_insn(unmodified_block, Insn::GuardAnyBitSet { val: block_handler, mask: Const::CUInt64(0x1), mask_name: None, reason: SideExitReason::BlockParamProxyFallbackMiss, state: exit_id });
76337668
// TODO(Shopify/ruby#753): GC root, so we should be able to avoid unnecessary GC tracing
76347669
let proxy_val = fun.push_insn(unmodified_block, Insn::Const { val: Const::Value(unsafe { rb_block_param_proxy }) });
7635-
let mut unmodified_args = vec![proxy_val];
7636-
if let Some(local) = original_local { unmodified_args.push(local); }
7637-
fun.push_insn(unmodified_block, Insn::Jump(BranchEdge { target: join_block, args: unmodified_args }));
7670+
let mut args = vec![proxy_val];
7671+
if let Some(local) = original_local {
7672+
args.push(local);
7673+
}
7674+
fun.push_insn(unmodified_block, Insn::Jump(BranchEdge { target: join_block, args }));
7675+
}
7676+
// A single supported profiled family. Emit a monomorphic fast path
7677+
[profiled_handler] => match profiled_handler {
7678+
ProfiledBlockHandlerFamily::Nil => {
7679+
fun.push_insn(unmodified_block, Insn::GuardBitEquals { val: block_handler, expected: Const::CInt64(VM_BLOCK_HANDLER_NONE.into()), reason: SideExitReason::BlockParamProxyNotNil, state: exit_id, recompile: None });
7680+
let nil_val = fun.push_insn(unmodified_block, Insn::Const { val: Const::Value(Qnil) });
7681+
let mut args = vec![nil_val];
7682+
if let Some(local) = original_local {
7683+
args.push(local);
7684+
}
7685+
fun.push_insn(unmodified_block, Insn::Jump(BranchEdge { target: join_block, args }));
7686+
}
7687+
ProfiledBlockHandlerFamily::IseqOrIfunc => {
7688+
// This handles two cases which are nearly identical.
7689+
// Block handler is a tagged pointer. Look at the tag.
7690+
// VM_BH_ISEQ_BLOCK_P(): block_handler & 0x03 == 0x01
7691+
// VM_BH_IFUNC_P(): block_handler & 0x03 == 0x03
7692+
// So to check for either of those cases we can use: val & 0x1 == 0x1
7693+
7694+
// Bail out if the block handler is neither ISEQ nor ifunc
7695+
fun.push_insn(unmodified_block, Insn::GuardAnyBitSet { val: block_handler, mask: Const::CUInt64(0x1), mask_name: None, reason: SideExitReason::BlockParamProxyNotIseqOrIfunc, state: exit_id });
7696+
// TODO(Shopify/ruby#753): GC root, so we should be able to avoid unnecessary GC tracing
7697+
let proxy_val = fun.push_insn(unmodified_block, Insn::Const { val: Const::Value(unsafe { rb_block_param_proxy }) });
7698+
let mut args = vec![proxy_val];
7699+
if let Some(local) = original_local {
7700+
args.push(local);
7701+
}
7702+
fun.push_insn(unmodified_block, Insn::Jump(BranchEdge { target: join_block, args }));
7703+
}
7704+
},
7705+
// Multiple supported profiled families. Emit a polymorphic dispatch
7706+
_ => {
7707+
let profiled_blocks = profiled_handlers.iter()
7708+
.map(|&kind| (kind, fun.new_block(branch_insn_idx)))
7709+
.collect::<Vec<_>>();
7710+
7711+
for &(kind, profiled_block) in &profiled_blocks {
7712+
match kind {
7713+
ProfiledBlockHandlerFamily::Nil => {
7714+
let none_handler = fun.push_insn(unmodified_block, Insn::Const {
7715+
val: Const::CInt64(VM_BLOCK_HANDLER_NONE.into()),
7716+
});
7717+
let is_none = fun.push_insn(unmodified_block, Insn::IsBitEqual {
7718+
left: block_handler,
7719+
right: none_handler,
7720+
});
7721+
fun.push_insn(unmodified_block, Insn::IfTrue {
7722+
val: is_none,
7723+
target: BranchEdge { target: profiled_block, args: vec![] },
7724+
});
7725+
let val = fun.push_insn(profiled_block, Insn::Const { val: Const::Value(Qnil) });
7726+
let mut args = vec![val];
7727+
if let Some(local) = original_local { args.push(local); }
7728+
fun.push_insn(profiled_block, Insn::Jump(BranchEdge { target: join_block, args }));
7729+
7730+
}
7731+
ProfiledBlockHandlerFamily::IseqOrIfunc => {
7732+
// This handles two cases which are nearly identical.
7733+
// Block handler is a tagged pointer. Look at the tag.
7734+
// VM_BH_ISEQ_BLOCK_P(): block_handler & 0x03 == 0x01
7735+
// VM_BH_IFUNC_P(): block_handler & 0x03 == 0x03
7736+
// So to check for either of those cases we can use: val & 0x1 == 0x1
7737+
let tag_mask = fun.push_insn(unmodified_block, Insn::Const { val: Const::CInt64(0x1) });
7738+
let tag_bits = fun.push_insn(unmodified_block, Insn::IntAnd {
7739+
left: block_handler,
7740+
right: tag_mask,
7741+
});
7742+
let is_iseq_or_ifunc = fun.push_insn(unmodified_block, Insn::IsBitEqual {
7743+
left: tag_bits,
7744+
right: tag_mask,
7745+
});
7746+
fun.push_insn(unmodified_block, Insn::IfTrue {
7747+
val: is_iseq_or_ifunc,
7748+
target: BranchEdge { target: profiled_block, args: vec![] },
7749+
});
7750+
// TODO(Shopify/ruby#753): GC root, so we should be able to avoid unnecessary GC tracing
7751+
let val = fun.push_insn(profiled_block, Insn::Const { val: Const::Value(unsafe { rb_block_param_proxy }) });
7752+
let mut args = vec![val];
7753+
if let Some(local) = original_local { args.push(local); }
7754+
fun.push_insn(profiled_block, Insn::Jump(BranchEdge { target: join_block, args }));
7755+
},
7756+
}
7757+
}
7758+
7759+
fun.push_insn(unmodified_block, Insn::SideExit { state: exit_id, reason: SideExitReason::BlockParamProxyProfileNotCovered, recompile: None });
76387760
}
76397761
}
76407762

zjit/src/hir/opt_tests.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4839,6 +4839,61 @@ mod hir_opt_tests {
48394839
");
48404840
}
48414841

4842+
#[test]
4843+
fn test_getblockparamproxy_polymorphic_none_and_iseq() {
4844+
set_call_threshold(3);
4845+
eval("
4846+
def test(&block)
4847+
0.then(&block)
4848+
end
4849+
4850+
test
4851+
test { 1 }
4852+
");
4853+
assert_contains_opcode("test", YARVINSN_getblockparamproxy);
4854+
assert_snapshot!(hir_string("test"), @"
4855+
fn test@<compiled>:3:
4856+
bb1():
4857+
EntryPoint interpreter
4858+
v1:BasicObject = LoadSelf
4859+
v2:CPtr = LoadSP
4860+
v3:BasicObject = LoadField v2, :block@0x1000
4861+
Jump bb3(v1, v3)
4862+
bb2():
4863+
EntryPoint JIT(0)
4864+
v6:BasicObject = LoadArg :self@0
4865+
v7:BasicObject = LoadArg :block@1
4866+
Jump bb3(v6, v7)
4867+
bb3(v9:BasicObject, v10:BasicObject):
4868+
v14:Fixnum[0] = Const Value(0)
4869+
v18:CPtr = GetEP 0
4870+
v19:CBool = IsBlockParamModified v18
4871+
IfTrue v19, bb4()
4872+
v24:CInt64 = LoadField v18, :_env_data_index_specval@0x1001
4873+
v25:CInt64[1] = Const CInt64(1)
4874+
v26:CInt64 = IntAnd v24, v25
4875+
v27:CBool = IsBitEqual v26, v25
4876+
IfTrue v27, bb7()
4877+
v31:CInt64[0] = Const CInt64(0)
4878+
v32:CBool = IsBitEqual v24, v31
4879+
IfTrue v32, bb8()
4880+
SideExit BlockParamProxyProfileNotCovered
4881+
bb4():
4882+
v22:BasicObject = LoadField v18, :block@0x1002
4883+
Jump bb6(v22, v22)
4884+
bb7():
4885+
v29:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008))
4886+
Jump bb6(v29, v10)
4887+
bb8():
4888+
v34:NilClass = Const Value(nil)
4889+
Jump bb6(v34, v10)
4890+
bb6(v16:BasicObject, v17:BasicObject):
4891+
v38:BasicObject = Send v14, &block, :then, v16 # SendFallbackReason: Complex argument passing
4892+
CheckInterrupts
4893+
Return v38
4894+
");
4895+
}
4896+
48424897
#[test]
48434898
fn test_getblockparam() {
48444899
eval("

zjit/src/hir/tests.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3552,6 +3552,63 @@ pub(crate) mod hir_build_tests {
35523552
");
35533553
}
35543554

3555+
#[test]
3556+
fn test_getblockparamproxy_polymorphic_none_and_iseq() {
3557+
set_call_threshold(3);
3558+
eval("
3559+
def test(&block)
3560+
0.then(&block)
3561+
end
3562+
3563+
test
3564+
test { 1 }
3565+
");
3566+
assert_contains_opcode("test", YARVINSN_getblockparamproxy);
3567+
assert_snapshot!(hir_string("test"), @"
3568+
fn test@<compiled>:3:
3569+
bb1():
3570+
EntryPoint interpreter
3571+
v1:BasicObject = LoadSelf
3572+
v2:CPtr = LoadSP
3573+
v3:BasicObject = LoadField v2, :block@0x1000
3574+
Jump bb3(v1, v3)
3575+
bb2():
3576+
EntryPoint JIT(0)
3577+
v6:BasicObject = LoadArg :self@0
3578+
v7:BasicObject = LoadArg :block@1
3579+
Jump bb3(v6, v7)
3580+
bb3(v9:BasicObject, v10:BasicObject):
3581+
v14:Fixnum[0] = Const Value(0)
3582+
v18:CPtr = GetEP 0
3583+
v19:CBool = IsBlockParamModified v18
3584+
IfTrue v19, bb4()
3585+
Jump bb5()
3586+
bb4():
3587+
v22:BasicObject = LoadField v18, :block@0x1001
3588+
Jump bb6(v22, v22)
3589+
bb5():
3590+
v24:CInt64 = LoadField v18, :_env_data_index_specval@0x1002
3591+
v25:CInt64[1] = Const CInt64(1)
3592+
v26:CInt64 = IntAnd v24, v25
3593+
v27:CBool = IsBitEqual v26, v25
3594+
IfTrue v27, bb7()
3595+
v31:CInt64[0] = Const CInt64(0)
3596+
v32:CBool = IsBitEqual v24, v31
3597+
IfTrue v32, bb8()
3598+
SideExit BlockParamProxyProfileNotCovered
3599+
bb7():
3600+
v29:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008))
3601+
Jump bb6(v29, v10)
3602+
bb8():
3603+
v34:NilClass = Const Value(nil)
3604+
Jump bb6(v34, v10)
3605+
bb6(v16:BasicObject, v17:BasicObject):
3606+
v38:BasicObject = Send v14, &block, :then, v16 # SendFallbackReason: Uncategorized(send)
3607+
CheckInterrupts
3608+
Return v38
3609+
");
3610+
}
3611+
35553612
#[test]
35563613
fn test_getblockparam_nested_block() {
35573614
eval("

zjit/src/stats.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,8 @@ make_counters! {
228228
exit_stackoverflow,
229229
exit_block_param_proxy_not_iseq_or_ifunc,
230230
exit_block_param_proxy_not_nil,
231+
exit_block_param_proxy_fallback_miss,
232+
exit_block_param_proxy_profile_not_covered,
231233
exit_block_param_wb_required,
232234
exit_too_many_keyword_parameters,
233235
exit_no_profile_send,
@@ -611,6 +613,8 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter {
611613
StackOverflow => exit_stackoverflow,
612614
BlockParamProxyNotIseqOrIfunc => exit_block_param_proxy_not_iseq_or_ifunc,
613615
BlockParamProxyNotNil => exit_block_param_proxy_not_nil,
616+
BlockParamProxyFallbackMiss => exit_block_param_proxy_fallback_miss,
617+
BlockParamProxyProfileNotCovered => exit_block_param_proxy_profile_not_covered,
614618
BlockParamWbRequired => exit_block_param_wb_required,
615619
TooManyKeywordParameters => exit_too_many_keyword_parameters,
616620
SplatKwNotNilOrHash => exit_splatkw_not_nil_or_hash,

0 commit comments

Comments
 (0)