Skip to content

Commit 0664b72

Browse files
committed
ZJIT: Optimize super calls to C function targets
1 parent acc4145 commit 0664b72

3 files changed

Lines changed: 260 additions & 78 deletions

File tree

test/ruby/test_zjit.rb

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,16 +1096,68 @@ def test
10961096
}, call_threshold: 2
10971097
end
10981098

1099-
def test_invokesuper_to_cfunc
1100-
assert_compiles '["MyArray", 3]', %q{
1101-
class MyArray < Array
1099+
def test_invokesuper_to_cfunc_no_args
1100+
assert_compiles '["MyString", 3]', %q{
1101+
class MyString < String
11021102
def length
1103-
["MyArray", super]
1103+
["MyString", super]
11041104
end
11051105
end
11061106
11071107
def test
1108-
MyArray.new([1, 2, 3]).length
1108+
MyString.new("abc").length
1109+
end
1110+
1111+
test # profile invokesuper
1112+
test # compile + run compiled code
1113+
}, call_threshold: 2
1114+
end
1115+
1116+
def test_invokesuper_to_cfunc_simple_args
1117+
assert_compiles '["MyString", true]', %q{
1118+
class MyString < String
1119+
def include?(other)
1120+
["MyString", super(other)]
1121+
end
1122+
end
1123+
1124+
def test
1125+
MyString.new("abc").include?("bc")
1126+
end
1127+
1128+
test # profile invokesuper
1129+
test # compile + run compiled code
1130+
}, call_threshold: 2
1131+
end
1132+
1133+
1134+
def test_invokesuper_to_cfunc_with_optional_arg
1135+
assert_compiles '["MyString", 6]', %q{
1136+
class MyString < String
1137+
def byteindex(needle, offset = 0)
1138+
["MyString", super(needle, offset)]
1139+
end
1140+
end
1141+
1142+
def test
1143+
MyString.new("hello world").byteindex("world")
1144+
end
1145+
1146+
test # profile invokesuper
1147+
test # compile + run compiled code
1148+
}, call_threshold: 2
1149+
end
1150+
1151+
def test_invokesuper_to_cfunc_varargs
1152+
assert_compiles '["MyString", true]', %q{
1153+
class MyString < String
1154+
def end_with?(str)
1155+
["MyString", super(str)]
1156+
end
1157+
end
1158+
1159+
def test
1160+
MyString.new("abc").end_with?("bc")
11091161
end
11101162
11111163
test # profile invokesuper

zjit/src/hir.rs

Lines changed: 142 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,6 +1297,20 @@ fn get_local_var_name_for_printer(iseq: Option<IseqPtr>, level: u32, ep_offset:
12971297
Some(format!(":{}", id.contents_lossy()))
12981298
}
12991299

1300+
/// Construct a qualified method name for display/debug output.
1301+
/// Returns strings like "Array#length" for instance methods or "Foo.bar" for singleton methods.
1302+
fn qualified_method_name(class: VALUE, method_id: ID) -> String {
1303+
let method_name = method_id.contents_lossy();
1304+
// rb_zjit_singleton_class_p also checks if it's a class
1305+
if unsafe { rb_zjit_singleton_class_p(class) } {
1306+
let class_name = get_class_name(unsafe { rb_class_attached_object(class) });
1307+
format!("{class_name}.{method_name}")
1308+
} else {
1309+
let class_name = get_class_name(class);
1310+
format!("{class_name}#{method_name}")
1311+
}
1312+
}
1313+
13001314
static REGEXP_FLAGS: &[(u32, &str)] = &[
13011315
(ONIG_OPTION_MULTILINE, "MULTILINE"),
13021316
(ONIG_OPTION_IGNORECASE, "IGNORECASE"),
@@ -3504,6 +3518,40 @@ impl Function {
35043518
};
35053519
}
35063520
Insn::InvokeSuper { recv, cd, blockiseq, args, state, .. } => {
3521+
// Helper to emit common guards for super call optimization.
3522+
fn emit_super_call_guards(
3523+
fun: &mut Function,
3524+
block: BlockId,
3525+
super_cme: *const rb_callable_method_entry_t,
3526+
current_cme: *const rb_callable_method_entry_t,
3527+
mid: ID,
3528+
state: InsnId,
3529+
) {
3530+
fun.push_insn(block, Insn::PatchPoint {
3531+
invariant: Invariant::MethodRedefined {
3532+
klass: unsafe { (*super_cme).defined_class },
3533+
method: mid,
3534+
cme: super_cme
3535+
},
3536+
state
3537+
});
3538+
3539+
let lep = fun.push_insn(block, Insn::GetLEP);
3540+
fun.push_insn(block, Insn::GuardSuperMethodEntry {
3541+
lep,
3542+
cme: current_cme,
3543+
state
3544+
});
3545+
3546+
let block_handler = fun.push_insn(block, Insn::GetBlockHandler { lep });
3547+
fun.push_insn(block, Insn::GuardBitEquals {
3548+
val: block_handler,
3549+
expected: Const::Value(VALUE(VM_BLOCK_HANDLER_NONE as usize)),
3550+
reason: SideExitReason::UnhandledBlockArg,
3551+
state
3552+
});
3553+
}
3554+
35073555
// Don't handle calls with literal blocks (e.g., super { ... })
35083556
if !blockiseq.is_null() {
35093557
self.push_insn_id(block, insn_id);
@@ -3567,68 +3615,107 @@ impl Function {
35673615
continue;
35683616
}
35693617

3570-
// Check if it's an ISEQ method; bail if it isn't.
35713618
let def_type = unsafe { get_cme_def_type(super_cme) };
3572-
if def_type != VM_METHOD_TYPE_ISEQ {
3573-
self.push_insn_id(block, insn_id);
3574-
self.set_dynamic_send_reason(insn_id, SuperNotOptimizedMethodType(MethodType::from(def_type)));
3575-
continue;
3576-
}
35773619

3578-
// Check if the super method's parameters support direct send.
3579-
// If not, we can't do direct dispatch.
3580-
let super_iseq = unsafe { get_def_iseq_ptr((*super_cme).def) };
3581-
// TODO: pass Option<blockiseq> to can_direct_send when we start specializing super { ... }
3582-
if !can_direct_send(self, block, super_iseq, ci, insn_id, args.as_slice(), None) {
3583-
self.push_insn_id(block, insn_id);
3584-
self.set_dynamic_send_reason(insn_id, SuperTargetComplexArgsPass);
3585-
continue;
3586-
}
3620+
if def_type == VM_METHOD_TYPE_ISEQ {
3621+
// Check if the super method's parameters support direct send.
3622+
// If not, we can't do direct dispatch.
3623+
let super_iseq = unsafe { get_def_iseq_ptr((*super_cme).def) };
3624+
// TODO: pass Option<blockiseq> to can_direct_send when we start specializing `super { ... }`.
3625+
if !can_direct_send(self, block, super_iseq, ci, insn_id, args.as_slice(), None) {
3626+
self.push_insn_id(block, insn_id);
3627+
self.set_dynamic_send_reason(insn_id, SuperTargetComplexArgsPass);
3628+
continue;
3629+
}
35873630

3588-
// Add PatchPoint for method redefinition.
3589-
self.push_insn(block, Insn::PatchPoint {
3590-
invariant: Invariant::MethodRedefined {
3591-
klass: unsafe { (*super_cme).defined_class },
3592-
method: mid,
3593-
cme: super_cme
3594-
},
3595-
state
3596-
});
3631+
let Ok((send_state, processed_args, kw_bits)) = self.prepare_direct_send_args(block, &args, ci, super_iseq, state)
3632+
.inspect_err(|&reason| self.set_dynamic_send_reason(insn_id, reason)) else {
3633+
self.push_insn_id(block, insn_id); continue;
3634+
};
35973635

3598-
// Guard that we're calling `super` from the expected method context.
3599-
let lep = self.push_insn(block, Insn::GetLEP);
3600-
self.push_insn(block, Insn::GuardSuperMethodEntry {
3601-
lep,
3602-
cme: current_cme,
3603-
state
3604-
});
3636+
emit_super_call_guards(self, block, super_cme, current_cme, mid, state);
36053637

3606-
// Guard that no block is being passed (implicit or explicit).
3607-
let block_handler = self.push_insn(block, Insn::GetBlockHandler { lep });
3608-
self.push_insn(block, Insn::GuardBitEquals {
3609-
val: block_handler,
3610-
expected: Const::Value(VALUE(VM_BLOCK_HANDLER_NONE as usize)),
3611-
reason: SideExitReason::UnhandledBlockArg,
3612-
state
3613-
});
3638+
// Use SendDirect with the super method's CME and ISEQ.
3639+
let send_direct = self.push_insn(block, Insn::SendDirect {
3640+
recv,
3641+
cd,
3642+
cme: super_cme,
3643+
iseq: super_iseq,
3644+
args: processed_args,
3645+
kw_bits,
3646+
state: send_state,
3647+
blockiseq: None,
3648+
});
3649+
self.make_equal_to(insn_id, send_direct);
36143650

3615-
let Ok((send_state, processed_args, kw_bits)) = self.prepare_direct_send_args(block, &args, ci, super_iseq, state)
3616-
.inspect_err(|&reason| self.set_dynamic_send_reason(insn_id, reason)) else {
3617-
self.push_insn_id(block, insn_id); continue;
3618-
};
3651+
} else if def_type == VM_METHOD_TYPE_CFUNC {
3652+
let cfunc = unsafe { get_cme_def_body_cfunc(super_cme) };
3653+
let cfunc_argc = unsafe { get_mct_argc(cfunc) };
3654+
let cfunc_ptr = unsafe { get_mct_func(cfunc) }.cast();
3655+
3656+
match cfunc_argc {
3657+
// C function with fixed argument count.
3658+
0.. => {
3659+
// Check argc matches
3660+
if args.len() != cfunc_argc as usize {
3661+
self.push_insn_id(block, insn_id);
3662+
self.set_dynamic_send_reason(insn_id, ArgcParamMismatch);
3663+
continue;
3664+
}
36193665

3620-
// Use SendDirect with the super method's CME and ISEQ.
3621-
let send_direct = self.push_insn(block, Insn::SendDirect {
3622-
recv,
3623-
cd,
3624-
cme: super_cme,
3625-
iseq: super_iseq,
3626-
args: processed_args,
3627-
kw_bits,
3628-
state: send_state,
3629-
blockiseq: None,
3630-
});
3631-
self.make_equal_to(insn_id, send_direct);
3666+
emit_super_call_guards(self, block, super_cme, current_cme, mid, state);
3667+
3668+
// Use CCallWithFrame for the C function.
3669+
let name = rust_str_to_id(&qualified_method_name(unsafe { (*super_cme).owner }, unsafe { (*super_cme).called_id }));
3670+
let ccall = self.push_insn(block, Insn::CCallWithFrame {
3671+
cd,
3672+
cfunc: cfunc_ptr,
3673+
recv,
3674+
args: args.clone(),
3675+
cme: super_cme,
3676+
name,
3677+
state,
3678+
return_type: types::BasicObject,
3679+
elidable: false,
3680+
blockiseq: None,
3681+
});
3682+
self.make_equal_to(insn_id, ccall);
3683+
}
3684+
3685+
// Variadic C function: func(int argc, VALUE *argv, VALUE recv)
3686+
-1 => {
3687+
emit_super_call_guards(self, block, super_cme, current_cme, mid, state);
3688+
3689+
// Use CCallVariadic for the variadic C function.
3690+
let name = rust_str_to_id(&qualified_method_name(unsafe { (*super_cme).owner }, unsafe { (*super_cme).called_id }));
3691+
let ccall = self.push_insn(block, Insn::CCallVariadic {
3692+
cfunc: cfunc_ptr,
3693+
recv,
3694+
args: args.clone(),
3695+
cme: super_cme,
3696+
name,
3697+
state,
3698+
return_type: types::BasicObject,
3699+
elidable: false,
3700+
blockiseq: None,
3701+
});
3702+
self.make_equal_to(insn_id, ccall);
3703+
}
3704+
3705+
// Array-variadic: (self, args_ruby_array).
3706+
-2 => {
3707+
self.push_insn_id(block, insn_id);
3708+
self.set_dynamic_send_reason(insn_id, SuperNotOptimizedMethodType(MethodType::Cfunc));
3709+
continue;
3710+
}
3711+
_ => unreachable!("unknown cfunc argc: {}", cfunc_argc)
3712+
}
3713+
} else {
3714+
// Other method types (not ISEQ or CFUNC)
3715+
self.push_insn_id(block, insn_id);
3716+
self.set_dynamic_send_reason(insn_id, SuperNotOptimizedMethodType(MethodType::from(def_type)));
3717+
continue;
3718+
}
36323719
}
36333720
_ => { self.push_insn_id(block, insn_id); }
36343721
}
@@ -4296,18 +4383,6 @@ impl Function {
42964383
Err(())
42974384
}
42984385

4299-
fn qualified_method_name(class: VALUE, method_id: ID) -> String {
4300-
let method_name = method_id.contents_lossy();
4301-
// rb_zjit_singleton_class_p also checks if it's a class
4302-
if unsafe { rb_zjit_singleton_class_p(class) } {
4303-
let class_name = get_class_name(unsafe { rb_class_attached_object(class) });
4304-
format!("{class_name}.{method_name}")
4305-
} else {
4306-
let class_name = get_class_name(class);
4307-
format!("{class_name}#{method_name}")
4308-
}
4309-
}
4310-
43114386
fn count_not_inlined_cfunc(fun: &mut Function, block: BlockId, cme: *const rb_callable_method_entry_t) {
43124387
let owner = unsafe { (*cme).owner };
43134388
let called_id = unsafe { (*cme).called_id };

0 commit comments

Comments
 (0)