Skip to content

Commit daa3578

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 d9acd0f commit daa3578

4 files changed

Lines changed: 178 additions & 97 deletions

File tree

test/ruby/test_zjit.rb

Lines changed: 30 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,35 @@ 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 '["png", {content: "image data"}]', %q{
1265+
class Base
1266+
def initialize(type, attributes = {})
1267+
@type = type
1268+
@attributes = attributes
1269+
end
1270+
1271+
def info
1272+
[@type, @attributes]
1273+
end
1274+
end
1275+
1276+
class Generic < Base
1277+
def initialize(type, content = "")
1278+
super(type, content: content)
1279+
end
1280+
end
1281+
1282+
def test
1283+
Generic.new("png", "image data").info
1284+
end
1285+
1286+
test
1287+
test
1288+
}, call_threshold: 2
1289+
end
1290+
12621291
def test_invokebuiltin
12631292
# Not using assert_compiles due to register spill
12641293
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: 42 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -895,17 +895,6 @@ pub enum Insn {
895895
state: InsnId,
896896
reason: SendFallbackReason,
897897
},
898-
/// Optimized super call to an ISEQ method
899-
InvokeSuperDirect {
900-
recv: InsnId,
901-
cd: *const rb_call_data,
902-
/// The CME of the method containing the super call (for runtime guard)
903-
current_cme: *const rb_callable_method_entry_t,
904-
/// The resolved super method's CME (for PatchPoint and to get target ISEQ)
905-
super_cme: *const rb_callable_method_entry_t,
906-
args: Vec<InsnId>,
907-
state: InsnId,
908-
},
909898
InvokeBlock {
910899
cd: *const rb_call_data,
911900
args: Vec<InsnId>,
@@ -982,6 +971,11 @@ pub enum Insn {
982971
GuardGreaterEq { left: InsnId, right: InsnId, state: InsnId },
983972
/// Side-exit if left is not less than right (both operands are C long).
984973
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,
985979

986980
/// Generate no code (or padding if necessary) and insert a patch point
987981
/// that can be rewritten to a side exit when the Invariant is broken.
@@ -1010,7 +1004,7 @@ impl Insn {
10101004
| Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::SetClassVar { .. } | Insn::ArrayExtend { .. }
10111005
| Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetGlobal { .. }
10121006
| Insn::SetLocal { .. } | Insn::Throw { .. } | Insn::IncrCounter(_) | Insn::IncrCounterPtr { .. }
1013-
| Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. }
1007+
| Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. } | Insn::GuardSuperMethodEntry { .. }
10141008
| Insn::StoreField { .. } | Insn::WriteBarrier { .. } | Insn::HashAset { .. }
10151009
| Insn::ArrayAset { .. } => false,
10161010
_ => true,
@@ -1310,13 +1304,6 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
13101304
write!(f, " # SendFallbackReason: {reason}")?;
13111305
Ok(())
13121306
}
1313-
Insn::InvokeSuperDirect { recv, args, .. } => {
1314-
write!(f, "InvokeSuperDirect {recv}")?;
1315-
for arg in args {
1316-
write!(f, ", {arg}")?;
1317-
}
1318-
Ok(())
1319-
}
13201307
Insn::InvokeBlock { args, reason, .. } => {
13211308
write!(f, "InvokeBlock")?;
13221309
for arg in args {
@@ -1364,6 +1351,8 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
13641351
Insn::GuardNotShared { recv, .. } => write!(f, "GuardNotShared {recv}"),
13651352
Insn::GuardLess { left, right, .. } => write!(f, "GuardLess {left}, {right}"),
13661353
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"),
13671356
Insn::PatchPoint { invariant, .. } => { write!(f, "PatchPoint {}", invariant.print(self.ptr_map)) },
13681357
Insn::GetConstantPath { ic, .. } => { write!(f, "GetConstantPath {:p}", self.ptr_map.map_ptr(ic)) },
13691358
Insn::IsBlockGiven => { write!(f, "IsBlockGiven") },
@@ -2001,6 +1990,8 @@ impl Function {
20011990
&GuardNotShared { recv, state } => GuardNotShared { recv: find!(recv), state },
20021991
&GuardGreaterEq { left, right, state } => GuardGreaterEq { left: find!(left), right: find!(right), state },
20031992
&GuardLess { left, right, state } => GuardLess { left: find!(left), right: find!(right), state },
1993+
&GuardSuperMethodEntry { cme, state } => GuardSuperMethodEntry { cme, state },
1994+
&GetBlockHandler => GetBlockHandler,
20041995
&FixnumAdd { left, right, state } => FixnumAdd { left: find!(left), right: find!(right), state },
20051996
&FixnumSub { left, right, state } => FixnumSub { left: find!(left), right: find!(right), state },
20061997
&FixnumMult { left, right, state } => FixnumMult { left: find!(left), right: find!(right), state },
@@ -2066,14 +2057,6 @@ impl Function {
20662057
state,
20672058
reason,
20682059
},
2069-
&InvokeSuperDirect { recv, cd, current_cme, super_cme, ref args, state } => InvokeSuperDirect {
2070-
recv: find!(recv),
2071-
cd,
2072-
current_cme,
2073-
super_cme,
2074-
args: find_vec!(args),
2075-
state,
2076-
},
20772060
&InvokeBlock { cd, ref args, state, reason } => InvokeBlock {
20782061
cd,
20792062
args: find_vec!(args),
@@ -2181,8 +2164,9 @@ impl Function {
21812164
Insn::SetGlobal { .. } | Insn::Jump(_) | Insn::EntryPoint { .. }
21822165
| Insn::IfTrue { .. } | Insn::IfFalse { .. } | Insn::Return { .. } | Insn::Throw { .. }
21832166
| Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::SetClassVar { .. } | Insn::ArrayExtend { .. }
2184-
| Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetLocal { .. } | Insn::IncrCounter(_)
2185-
| 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 { .. }
21862170
| Insn::StoreField { .. } | Insn::WriteBarrier { .. } | Insn::HashAset { .. } | Insn::ArrayAset { .. } =>
21872171
panic!("Cannot infer type of instruction with no output: {}. See Insn::has_output().", self.insns[insn.0]),
21882172
Insn::Const { val: Const::Value(val) } => Type::from_value(*val),
@@ -2262,7 +2246,6 @@ impl Function {
22622246
Insn::Send { .. } => types::BasicObject,
22632247
Insn::SendForward { .. } => types::BasicObject,
22642248
Insn::InvokeSuper { .. } => types::BasicObject,
2265-
Insn::InvokeSuperDirect { .. } => types::BasicObject,
22662249
Insn::InvokeBlock { .. } => types::BasicObject,
22672250
Insn::InvokeBuiltin { return_type, .. } => return_type.unwrap_or(types::BasicObject),
22682251
Insn::Defined { pushval, .. } => Type::from_value(*pushval).union(types::NilClass),
@@ -2290,6 +2273,7 @@ impl Function {
22902273
Insn::AnyToString { .. } => types::String,
22912274
Insn::GetLocal { rest_param: true, .. } => types::ArrayExact,
22922275
Insn::GetLocal { .. } => types::BasicObject,
2276+
Insn::GetBlockHandler => types::RubyValue,
22932277
// The type of Snapshot doesn't really matter; it's never materialized. It's used only
22942278
// as a reference for FrameState, which we use to generate side-exit code.
22952279
Insn::Snapshot { .. } => types::Any,
@@ -3044,16 +3028,17 @@ impl Function {
30443028
self.push_insn_id(block, insn_id); continue;
30453029
}
30463030

3047-
// 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.
30483033
let ci = unsafe { get_call_data_ci(cd) };
30493034
let flags = unsafe { rb_vm_ci_flag(ci) };
3050-
if unspecializable_call_type(flags) {
3035+
if (flags & VM_CALL_ZSUPER) == 0 {
30513036
self.push_insn_id(block, insn_id); continue;
30523037
}
30533038

30543039
let frame_state = self.frame_state(state);
30553040

3056-
// Get the profiled CME from the current method
3041+
// Get the profiled CME from the current method.
30573042
let Some(profiles) = self.profiles.as_ref() else {
30583043
self.push_insn_id(block, insn_id); continue;
30593044
};
@@ -3062,7 +3047,7 @@ impl Function {
30623047
self.push_insn_id(block, insn_id); continue;
30633048
};
30643049

3065-
// Get defined_class and method ID from the profiled CME
3050+
// Get defined_class and method ID from the profiled CME.
30663051
let current_defined_class = unsafe { (*current_cme).defined_class };
30673052
let mid = unsafe { get_def_original_id((*current_cme).def) };
30683053

@@ -3072,16 +3057,15 @@ impl Function {
30723057
self.push_insn_id(block, insn_id); continue;
30733058
}
30743059

3075-
// Look up the super method
3060+
// Look up the super method.
30763061
let super_cme = unsafe { rb_callable_method_entry(superclass, mid) };
30773062
if super_cme.is_null() {
30783063
self.push_insn_id(block, insn_id); continue;
30793064
}
30803065

3081-
// Check if it's an ISEQ method
3066+
// Check if it's an ISEQ method; bail if it isn't.
30823067
let def_type = unsafe { get_cme_def_type(super_cme) };
30833068
if def_type != VM_METHOD_TYPE_ISEQ {
3084-
// TODO: Handle CFUNCs
30853069
self.push_insn_id(block, insn_id); continue;
30863070
}
30873071

@@ -3092,8 +3076,7 @@ impl Function {
30923076
self.push_insn_id(block, insn_id); continue;
30933077
}
30943078

3095-
// Add PatchPoints for method redefinition
3096-
// TODO: Add guard that ep[-2] matches current_cme
3079+
// Add PatchPoint for method redefinition.
30973080
self.push_insn(block, Insn::PatchPoint {
30983081
invariant: Invariant::MethodRedefined {
30993082
klass: unsafe { (*super_cme).defined_class },
@@ -3103,11 +3086,23 @@ impl Function {
31033086
state
31043087
});
31053088

3106-
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+
state
3098+
});
3099+
3100+
// Use SendWithoutBlockDirect with the super method's CME and ISEQ.
3101+
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect {
31073102
recv,
31083103
cd,
3109-
current_cme,
3110-
super_cme,
3104+
cme: super_cme,
3105+
iseq: super_iseq,
31113106
args,
31123107
state
31133108
});
@@ -3975,6 +3970,7 @@ impl Function {
39753970
| &Insn::LoadEC
39763971
| &Insn::LoadSelf
39773972
| &Insn::GetLocal { .. }
3973+
| &Insn::GetBlockHandler
39783974
| &Insn::PutSpecialObject { .. }
39793975
| &Insn::IsBlockGiven
39803976
| &Insn::IncrCounter(_)
@@ -4151,8 +4147,7 @@ impl Function {
41514147
| &Insn::CCallWithFrame { recv, ref args, state, .. }
41524148
| &Insn::SendWithoutBlockDirect { recv, ref args, state, .. }
41534149
| &Insn::InvokeBuiltin { recv, ref args, state, .. }
4154-
| &Insn::InvokeSuper { recv, ref args, state, .. }
4155-
| &Insn::InvokeSuperDirect { recv, ref args, state, .. } => {
4150+
| &Insn::InvokeSuper { recv, ref args, state, .. } => {
41564151
worklist.push_back(recv);
41574152
worklist.extend(args);
41584153
worklist.push_back(state);
@@ -4204,6 +4199,7 @@ impl Function {
42044199
worklist.push_back(val);
42054200
}
42064201
&Insn::GuardBlockParamProxy { state, .. } |
4202+
&Insn::GuardSuperMethodEntry { state, .. } |
42074203
&Insn::GetGlobal { state, .. } |
42084204
&Insn::GetSpecialSymbol { state, .. } |
42094205
&Insn::GetSpecialNumber { state, .. } |
@@ -4715,6 +4711,8 @@ impl Function {
47154711
| Insn::Jump { .. }
47164712
| Insn::EntryPoint { .. }
47174713
| Insn::GuardBlockParamProxy { .. }
4714+
| Insn::GuardSuperMethodEntry { .. }
4715+
| Insn::GetBlockHandler
47184716
| Insn::PatchPoint { .. }
47194717
| Insn::SideExit { .. }
47204718
| Insn::IncrCounter { .. }
@@ -4768,7 +4766,6 @@ impl Function {
47684766
| Insn::Send { recv, ref args, .. }
47694767
| Insn::SendForward { recv, ref args, .. }
47704768
| Insn::InvokeSuper { recv, ref args, .. }
4771-
| Insn::InvokeSuperDirect { recv, ref args, .. }
47724769
| Insn::CCallWithFrame { recv, ref args, .. }
47734770
| Insn::CCallVariadic { recv, ref args, .. }
47744771
| Insn::InvokeBuiltin { recv, ref args, .. }

0 commit comments

Comments
 (0)