Skip to content

Commit 166d4d8

Browse files
committed
ZJIT: Simplify invokesuper specialization to most common case
Looking at ruby-bench, most `super` calls don't pass a block, which means we can use the already optimized `SendWithoutBlockDirect`.
1 parent 444b14c commit 166d4d8

File tree

5 files changed

+206
-97
lines changed

5 files changed

+206
-97
lines changed

test/ruby/test_zjit.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1241,7 +1241,7 @@ def foo
12411241
end
12421242
12431243
def test
1244-
B.new.f
1244+
B.new.foo
12451245
end
12461246
12471247
test # profile invokesuper (super -> A#foo)
@@ -1259,6 +1259,30 @@ def foo
12591259
}, call_threshold: 2
12601260
end
12611261

1262+
# Test super with positional and keyword arguments (pattern from chunky_png)
1263+
def test_invokesuper_with_keyword_args
1264+
assert_compiles '{content: "image data"}', %q{
1265+
class A
1266+
def foo(attributes = {})
1267+
@attributes = attributes
1268+
end
1269+
end
1270+
1271+
class B < A
1272+
def foo(content = '')
1273+
super(content: content)
1274+
end
1275+
end
1276+
1277+
def test
1278+
B.new.foo("image data")
1279+
end
1280+
1281+
test
1282+
test
1283+
}, call_threshold: 2
1284+
end
1285+
12621286
def test_invokebuiltin
12631287
# Not using assert_compiles due to register spill
12641288
assert_runs '["."]', %q{

zjit/src/codegen.rs

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,6 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
406406
gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), SendFallbackReason::TooManyArgsForLir),
407407
Insn::SendWithoutBlockDirect { cme, iseq, recv, args, state, .. } => gen_send_iseq_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), &function.frame_state(*state), None),
408408
&Insn::InvokeSuper { cd, blockiseq, state, reason, .. } => gen_invokesuper(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
409-
Insn::InvokeSuperDirect { recv, current_cme, super_cme, args, state, .. } =>
410-
gen_invokesuper_direct(cb, jit, asm, *current_cme, *super_cme, opnd!(recv), opnds!(args), &function.frame_state(*state)),
411409
&Insn::InvokeBlock { cd, state, reason, .. } => gen_invokeblock(jit, asm, cd, &function.frame_state(state), reason),
412410
// Ensure we have enough room fit ec, self, and arguments
413411
// TODO remove this check when we have stack args (we can use Time.new to test it)
@@ -457,6 +455,8 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
457455
Insn::GuardNotShared { recv, state } => gen_guard_not_shared(jit, asm, opnd!(recv), &function.frame_state(*state)),
458456
&Insn::GuardLess { left, right, state } => gen_guard_less(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)),
459457
&Insn::GuardGreaterEq { left, right, state } => gen_guard_greater_eq(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)),
458+
&Insn::GuardSuperMethodEntry { cme, state } => no_output!(gen_guard_super_method_entry(jit, asm, cme, &function.frame_state(state))),
459+
Insn::GetBlockHandler => gen_get_block_handler(jit, asm),
460460
Insn::PatchPoint { invariant, state } => no_output!(gen_patch_point(jit, asm, invariant, &function.frame_state(*state))),
461461
Insn::CCall { cfunc, recv, args, name, return_type: _, elidable: _ } => gen_ccall(asm, *cfunc, *name, opnd!(recv), opnds!(args)),
462462
// Give up CCallWithFrame for 7+ args since asm.ccall() supports at most 6 args (recv + args).
@@ -719,6 +719,29 @@ fn gen_guard_greater_eq(jit: &JITState, asm: &mut Assembler, left: Opnd, right:
719719
left
720720
}
721721

722+
/// Guard that the method entry at ep[VM_ENV_DATA_INDEX_ME_CREF] matches the expected CME.
723+
/// This ensures we're calling super from the expected method context.
724+
fn gen_guard_super_method_entry(
725+
jit: &JITState,
726+
asm: &mut Assembler,
727+
cme: *const rb_callable_method_entry_t,
728+
state: &FrameState,
729+
) {
730+
asm_comment!(asm, "guard super method entry");
731+
let lep = gen_get_lep(jit, asm);
732+
let ep_me_opnd = Opnd::mem(64, lep, SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_ME_CREF);
733+
let ep_me = asm.load(ep_me_opnd);
734+
asm.cmp(ep_me, Opnd::UImm(cme as u64));
735+
asm.jne(side_exit(jit, state, SideExitReason::GuardSuperMethodEntry));
736+
}
737+
738+
/// Get the block handler from ep[VM_ENV_DATA_INDEX_SPECVAL] at the local EP (LEP).
739+
fn gen_get_block_handler(jit: &JITState, asm: &mut Assembler) -> Opnd {
740+
asm_comment!(asm, "get block handler from LEP");
741+
let lep = gen_get_lep(jit, asm);
742+
asm.load(Opnd::mem(64, lep, SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL))
743+
}
744+
722745
fn gen_get_constant_path(jit: &JITState, asm: &mut Assembler, ic: *const iseq_inline_constant_cache, state: &FrameState) -> Opnd {
723746
unsafe extern "C" {
724747
fn rb_vm_opt_getconstant_path(ec: EcPtr, cfp: CfpPtr, ic: *const iseq_inline_constant_cache) -> VALUE;
@@ -1499,37 +1522,6 @@ fn gen_invokesuper(
14991522
)
15001523
}
15011524

1502-
/// Compile an optimized super call to an ISEQ method
1503-
fn gen_invokesuper_direct(
1504-
cb: &mut CodeBlock,
1505-
jit: &mut JITState,
1506-
asm: &mut Assembler,
1507-
current_cme: *const rb_callable_method_entry_t,
1508-
super_cme: *const rb_callable_method_entry_t,
1509-
recv: lir::Opnd,
1510-
args: Vec<lir::Opnd>,
1511-
state: &FrameState,
1512-
) -> lir::Opnd {
1513-
// Guard that ep[VM_ENV_DATA_INDEX_ME_CREF] matches current_cme, ensuring that we're calling
1514-
// `super` from the expected method context.
1515-
asm_comment!(asm, "guard super method entry");
1516-
let lep = gen_get_lep(jit, asm);
1517-
let ep_me_opnd = Opnd::mem(64, lep, SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_ME_CREF);
1518-
let ep_me = asm.load(ep_me_opnd);
1519-
asm.cmp(ep_me, Opnd::UImm(current_cme as u64));
1520-
asm.jne(side_exit(jit, state, SideExitReason::GuardSuperMethodEntry));
1521-
1522-
// Get the super method's ISEQ since we'll be dispatching to that.
1523-
let super_iseq = unsafe { get_def_iseq_ptr((*super_cme).def) };
1524-
1525-
// Read the block handler from the LEP to forward the caller's block.
1526-
asm_comment!(asm, "load block handler from LEP");
1527-
let block_handler = asm.load(Opnd::mem(64, lep, SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL));
1528-
1529-
// Generate the code to invoke the `super` target method.
1530-
gen_send_iseq_direct(cb, jit, asm, super_cme, super_iseq, recv, args, state, Some(block_handler))
1531-
}
1532-
15331525
/// Compile a string resurrection
15341526
fn gen_string_copy(asm: &mut Assembler, recv: Opnd, chilled: bool, state: &FrameState) -> Opnd {
15351527
// TODO: split rb_ec_str_resurrect into separate functions

zjit/src/hir.rs

Lines changed: 44 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ pub enum SideExitReason {
483483
UnhandledHIRInsn(InsnId),
484484
UnhandledYARVInsn(u32),
485485
UnhandledCallType(CallType),
486+
UnhandledBlockArg,
486487
TooManyKeywordParameters,
487488
FixnumAddOverflow,
488489
FixnumSubOverflow,
@@ -894,17 +895,6 @@ pub enum Insn {
894895
state: InsnId,
895896
reason: SendFallbackReason,
896897
},
897-
/// Optimized super call to an ISEQ method
898-
InvokeSuperDirect {
899-
recv: InsnId,
900-
cd: *const rb_call_data,
901-
/// The CME of the method containing the super call (for runtime guard)
902-
current_cme: *const rb_callable_method_entry_t,
903-
/// The resolved super method's CME (for PatchPoint and to get target ISEQ)
904-
super_cme: *const rb_callable_method_entry_t,
905-
args: Vec<InsnId>,
906-
state: InsnId,
907-
},
908898
InvokeBlock {
909899
cd: *const rb_call_data,
910900
args: Vec<InsnId>,
@@ -981,6 +971,11 @@ pub enum Insn {
981971
GuardGreaterEq { left: InsnId, right: InsnId, state: InsnId },
982972
/// Side-exit if left is not less than right (both operands are C long).
983973
GuardLess { left: InsnId, right: InsnId, state: InsnId },
974+
/// Side-exit if the method entry at ep[VM_ENV_DATA_INDEX_ME_CREF] doesn't match the expected CME.
975+
/// Used to ensure super calls are made from the expected method context.
976+
GuardSuperMethodEntry { cme: *const rb_callable_method_entry_t, state: InsnId },
977+
/// Get the block handler from ep[VM_ENV_DATA_INDEX_SPECVAL] at the local EP (LEP).
978+
GetBlockHandler,
984979

985980
/// Generate no code (or padding if necessary) and insert a patch point
986981
/// that can be rewritten to a side exit when the Invariant is broken.
@@ -1009,7 +1004,7 @@ impl Insn {
10091004
| Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::SetClassVar { .. } | Insn::ArrayExtend { .. }
10101005
| Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetGlobal { .. }
10111006
| Insn::SetLocal { .. } | Insn::Throw { .. } | Insn::IncrCounter(_) | Insn::IncrCounterPtr { .. }
1012-
| Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. }
1007+
| Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. } | Insn::GuardSuperMethodEntry { .. }
10131008
| Insn::StoreField { .. } | Insn::WriteBarrier { .. } | Insn::HashAset { .. }
10141009
| Insn::ArrayAset { .. } => false,
10151010
_ => true,
@@ -1309,13 +1304,6 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
13091304
write!(f, " # SendFallbackReason: {reason}")?;
13101305
Ok(())
13111306
}
1312-
Insn::InvokeSuperDirect { recv, args, .. } => {
1313-
write!(f, "InvokeSuperDirect {recv}")?;
1314-
for arg in args {
1315-
write!(f, ", {arg}")?;
1316-
}
1317-
Ok(())
1318-
}
13191307
Insn::InvokeBlock { args, reason, .. } => {
13201308
write!(f, "InvokeBlock")?;
13211309
for arg in args {
@@ -1363,6 +1351,8 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
13631351
Insn::GuardNotShared { recv, .. } => write!(f, "GuardNotShared {recv}"),
13641352
Insn::GuardLess { left, right, .. } => write!(f, "GuardLess {left}, {right}"),
13651353
Insn::GuardGreaterEq { left, right, .. } => write!(f, "GuardGreaterEq {left}, {right}"),
1354+
Insn::GuardSuperMethodEntry { cme, .. } => write!(f, "GuardSuperMethodEntry {:p}", self.ptr_map.map_ptr(cme)),
1355+
Insn::GetBlockHandler => write!(f, "GetBlockHandler"),
13661356
Insn::PatchPoint { invariant, .. } => { write!(f, "PatchPoint {}", invariant.print(self.ptr_map)) },
13671357
Insn::GetConstantPath { ic, .. } => { write!(f, "GetConstantPath {:p}", self.ptr_map.map_ptr(ic)) },
13681358
Insn::IsBlockGiven => { write!(f, "IsBlockGiven") },
@@ -2000,6 +1990,8 @@ impl Function {
20001990
&GuardNotShared { recv, state } => GuardNotShared { recv: find!(recv), state },
20011991
&GuardGreaterEq { left, right, state } => GuardGreaterEq { left: find!(left), right: find!(right), state },
20021992
&GuardLess { left, right, state } => GuardLess { left: find!(left), right: find!(right), state },
1993+
&GuardSuperMethodEntry { cme, state } => GuardSuperMethodEntry { cme, state },
1994+
&GetBlockHandler => GetBlockHandler,
20031995
&FixnumAdd { left, right, state } => FixnumAdd { left: find!(left), right: find!(right), state },
20041996
&FixnumSub { left, right, state } => FixnumSub { left: find!(left), right: find!(right), state },
20051997
&FixnumMult { left, right, state } => FixnumMult { left: find!(left), right: find!(right), state },
@@ -2065,14 +2057,6 @@ impl Function {
20652057
state,
20662058
reason,
20672059
},
2068-
&InvokeSuperDirect { recv, cd, current_cme, super_cme, ref args, state } => InvokeSuperDirect {
2069-
recv: find!(recv),
2070-
cd,
2071-
current_cme,
2072-
super_cme,
2073-
args: find_vec!(args),
2074-
state,
2075-
},
20762060
&InvokeBlock { cd, ref args, state, reason } => InvokeBlock {
20772061
cd,
20782062
args: find_vec!(args),
@@ -2180,8 +2164,9 @@ impl Function {
21802164
Insn::SetGlobal { .. } | Insn::Jump(_) | Insn::EntryPoint { .. }
21812165
| Insn::IfTrue { .. } | Insn::IfFalse { .. } | Insn::Return { .. } | Insn::Throw { .. }
21822166
| Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::SetClassVar { .. } | Insn::ArrayExtend { .. }
2183-
| Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetLocal { .. } | Insn::IncrCounter(_)
2184-
| Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. } | Insn::IncrCounterPtr { .. }
2167+
| Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetLocal { .. }
2168+
| Insn::IncrCounter(_) | Insn::IncrCounterPtr { .. }
2169+
| Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. } | Insn::GuardSuperMethodEntry { .. }
21852170
| Insn::StoreField { .. } | Insn::WriteBarrier { .. } | Insn::HashAset { .. } | Insn::ArrayAset { .. } =>
21862171
panic!("Cannot infer type of instruction with no output: {}. See Insn::has_output().", self.insns[insn.0]),
21872172
Insn::Const { val: Const::Value(val) } => Type::from_value(*val),
@@ -2261,7 +2246,6 @@ impl Function {
22612246
Insn::Send { .. } => types::BasicObject,
22622247
Insn::SendForward { .. } => types::BasicObject,
22632248
Insn::InvokeSuper { .. } => types::BasicObject,
2264-
Insn::InvokeSuperDirect { .. } => types::BasicObject,
22652249
Insn::InvokeBlock { .. } => types::BasicObject,
22662250
Insn::InvokeBuiltin { return_type, .. } => return_type.unwrap_or(types::BasicObject),
22672251
Insn::Defined { pushval, .. } => Type::from_value(*pushval).union(types::NilClass),
@@ -2289,6 +2273,7 @@ impl Function {
22892273
Insn::AnyToString { .. } => types::String,
22902274
Insn::GetLocal { rest_param: true, .. } => types::ArrayExact,
22912275
Insn::GetLocal { .. } => types::BasicObject,
2276+
Insn::GetBlockHandler => types::RubyValue,
22922277
// The type of Snapshot doesn't really matter; it's never materialized. It's used only
22932278
// as a reference for FrameState, which we use to generate side-exit code.
22942279
Insn::Snapshot { .. } => types::Any,
@@ -3043,16 +3028,17 @@ impl Function {
30433028
self.push_insn_id(block, insn_id); continue;
30443029
}
30453030

3046-
// Don't handle calls with complex arguments (kwarg, splat, kw_splat, blockarg, forwarding)
3031+
// Only handle bare `super` calls (i.e., with no arguments) so we can
3032+
// forward the caller arguments without needing to modify them.
30473033
let ci = unsafe { get_call_data_ci(cd) };
30483034
let flags = unsafe { rb_vm_ci_flag(ci) };
3049-
if unspecializable_call_type(flags) {
3035+
if (flags & VM_CALL_ZSUPER) == 0 || (flags & VM_CALL_ARGS_SIMPLE) == 0 {
30503036
self.push_insn_id(block, insn_id); continue;
30513037
}
30523038

30533039
let frame_state = self.frame_state(state);
30543040

3055-
// Get the profiled CME from the current method
3041+
// Get the profiled CME from the current method.
30563042
let Some(profiles) = self.profiles.as_ref() else {
30573043
self.push_insn_id(block, insn_id); continue;
30583044
};
@@ -3061,7 +3047,7 @@ impl Function {
30613047
self.push_insn_id(block, insn_id); continue;
30623048
};
30633049

3064-
// Get defined_class and method ID from the profiled CME
3050+
// Get defined_class and method ID from the profiled CME.
30653051
let current_defined_class = unsafe { (*current_cme).defined_class };
30663052
let mid = unsafe { get_def_original_id((*current_cme).def) };
30673053

@@ -3071,16 +3057,15 @@ impl Function {
30713057
self.push_insn_id(block, insn_id); continue;
30723058
}
30733059

3074-
// Look up the super method
3060+
// Look up the super method.
30753061
let super_cme = unsafe { rb_callable_method_entry(superclass, mid) };
30763062
if super_cme.is_null() {
30773063
self.push_insn_id(block, insn_id); continue;
30783064
}
30793065

3080-
// Check if it's an ISEQ method
3066+
// Check if it's an ISEQ method; bail if it isn't.
30813067
let def_type = unsafe { get_cme_def_type(super_cme) };
30823068
if def_type != VM_METHOD_TYPE_ISEQ {
3083-
// TODO: Handle CFUNCs
30843069
self.push_insn_id(block, insn_id); continue;
30853070
}
30863071

@@ -3091,8 +3076,7 @@ impl Function {
30913076
self.push_insn_id(block, insn_id); continue;
30923077
}
30933078

3094-
// Add PatchPoints for method redefinition
3095-
// TODO: Add guard that ep[-2] matches current_cme
3079+
// Add PatchPoint for method redefinition.
30963080
self.push_insn(block, Insn::PatchPoint {
30973081
invariant: Invariant::MethodRedefined {
30983082
klass: unsafe { (*super_cme).defined_class },
@@ -3102,11 +3086,24 @@ impl Function {
31023086
state
31033087
});
31043088

3105-
let send_direct = self.push_insn(block, Insn::InvokeSuperDirect {
3089+
// Guard that we're calling `super` from the expected method context.
3090+
self.push_insn(block, Insn::GuardSuperMethodEntry { cme: current_cme, state });
3091+
3092+
// Guard that no block is being passed (implicit or explicit).
3093+
let block_handler = self.push_insn(block, Insn::GetBlockHandler);
3094+
self.push_insn(block, Insn::GuardBitEquals {
3095+
val: block_handler,
3096+
expected: Const::Value(VALUE(VM_BLOCK_HANDLER_NONE as usize)),
3097+
reason: SideExitReason::UnhandledBlockArg,
3098+
state
3099+
});
3100+
3101+
// Use SendWithoutBlockDirect with the super method's CME and ISEQ.
3102+
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect {
31063103
recv,
31073104
cd,
3108-
current_cme,
3109-
super_cme,
3105+
cme: super_cme,
3106+
iseq: super_iseq,
31103107
args,
31113108
state
31123109
});
@@ -3995,6 +3992,7 @@ impl Function {
39953992
| &Insn::LoadEC
39963993
| &Insn::LoadSelf
39973994
| &Insn::GetLocal { .. }
3995+
| &Insn::GetBlockHandler
39983996
| &Insn::PutSpecialObject { .. }
39993997
| &Insn::IsBlockGiven
40003998
| &Insn::IncrCounter(_)
@@ -4171,8 +4169,7 @@ impl Function {
41714169
| &Insn::CCallWithFrame { recv, ref args, state, .. }
41724170
| &Insn::SendWithoutBlockDirect { recv, ref args, state, .. }
41734171
| &Insn::InvokeBuiltin { recv, ref args, state, .. }
4174-
| &Insn::InvokeSuper { recv, ref args, state, .. }
4175-
| &Insn::InvokeSuperDirect { recv, ref args, state, .. } => {
4172+
| &Insn::InvokeSuper { recv, ref args, state, .. } => {
41764173
worklist.push_back(recv);
41774174
worklist.extend(args);
41784175
worklist.push_back(state);
@@ -4224,6 +4221,7 @@ impl Function {
42244221
worklist.push_back(val);
42254222
}
42264223
&Insn::GuardBlockParamProxy { state, .. } |
4224+
&Insn::GuardSuperMethodEntry { state, .. } |
42274225
&Insn::GetGlobal { state, .. } |
42284226
&Insn::GetSpecialSymbol { state, .. } |
42294227
&Insn::GetSpecialNumber { state, .. } |
@@ -4735,6 +4733,8 @@ impl Function {
47354733
| Insn::Jump { .. }
47364734
| Insn::EntryPoint { .. }
47374735
| Insn::GuardBlockParamProxy { .. }
4736+
| Insn::GuardSuperMethodEntry { .. }
4737+
| Insn::GetBlockHandler
47384738
| Insn::PatchPoint { .. }
47394739
| Insn::SideExit { .. }
47404740
| Insn::IncrCounter { .. }
@@ -4788,7 +4788,6 @@ impl Function {
47884788
| Insn::Send { recv, ref args, .. }
47894789
| Insn::SendForward { recv, ref args, .. }
47904790
| Insn::InvokeSuper { recv, ref args, .. }
4791-
| Insn::InvokeSuperDirect { recv, ref args, .. }
47924791
| Insn::CCallWithFrame { recv, ref args, .. }
47934792
| Insn::CCallVariadic { recv, ref args, .. }
47944793
| Insn::InvokeBuiltin { recv, ref args, .. }

0 commit comments

Comments
 (0)