Skip to content

Commit 216c5eb

Browse files
tenderloveXrXr
andauthored
ZJIT: Support nil block parameters (ruby#16492)
* ZJIT: Support nil block parameters zjit-stats for the ActiveRecord benchmark used to look like this: ``` Top-8 popular complex argument-parameter features not optimized (100.0% of total 2,351,376): param_block: 2,157,030 (91.7%) param_forwardable: 77,125 ( 3.3%) param_rest: 35,052 ( 1.5%) param_kwrest: 28,827 ( 1.2%) caller_splat: 27,264 ( 1.2%) caller_blockarg: 25,309 ( 1.1%) caller_kwarg: 476 ( 0.0%) caller_kw_splat: 293 ( 0.0%) ``` This is because we weren't handling the case where a callee takes a block parameter, but the caller doesn't pass one. For example ```ruby def test(&blk); blk ? blk.call : 42; end test test ``` This patch adds support for `nil` block parameters to bmethod methods as well as iseq methods. Now ActiveRecord benchmark stats look like this: ``` Top-7 popular complex argument-parameter features not optimized (100.0% of total 192,204): param_forwardable: 74,983 (39.0%) param_rest: 35,052 (18.2%) param_kwrest: 28,827 (15.0%) caller_splat: 27,264 (14.2%) caller_blockarg: 25,309 (13.2%) caller_kwarg: 476 ( 0.2%) caller_kw_splat: 293 ( 0.2%) ``` For posterity: when the callee method is an iseq method, we already handled the "no block" case by correctly setting BLOCK_HANDLER_NONE in the environment. When the callee method is a bmethod, we need to initialize the block to nil as a local * amend check for complex_arg_pass_param_block instead of deleting (call to bmethods with a block is rejected elsewhere, but this makes it so we weaken the check just enough to admit the no block, callee block param case) * replace code that directly contradicts comment with an assert * add snapshot tests --------- Co-authored-by: Alan Wu <XrXr@users.noreply.github.com>
1 parent 6ae062a commit 216c5eb

File tree

3 files changed

+83
-11
lines changed

3 files changed

+83
-11
lines changed

zjit/src/codegen.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,7 +1503,9 @@ fn gen_send_iseq_direct(
15031503
// `unspecializable_call_type`.
15041504
let block_handler = blockiseq.map(|b| gen_block_handler_specval(asm, b));
15051505

1506-
let (frame_type, specval) = if VM_METHOD_TYPE_BMETHOD == unsafe { get_cme_def_type(cme) } {
1506+
let callee_is_bmethod = VM_METHOD_TYPE_BMETHOD == unsafe { get_cme_def_type(cme) };
1507+
1508+
let (frame_type, specval) = if callee_is_bmethod {
15071509
// Extract EP from the Proc instance
15081510
let procv = unsafe { rb_get_def_bmethod_proc((*cme).def) };
15091511
let proc = unsafe { rb_jit_get_proc_ptr(procv) };
@@ -1570,7 +1572,14 @@ fn gen_send_iseq_direct(
15701572
c_args.push(recv);
15711573
c_args.extend(&args);
15721574
if needs_block {
1573-
c_args.push(specval);
1575+
if callee_is_bmethod {
1576+
// For bmethods, specval is the captured EP, not the block handler.
1577+
// The block param needs nil (no block) or a Proc value.
1578+
assert!(block_handler.is_none(), "at the moment, HIR builder never emits a direct send for a to-bmethod send-with-literal-block");
1579+
c_args.push(Qnil.into());
1580+
} else {
1581+
c_args.push(specval);
1582+
}
15741583
}
15751584

15761585
let num_optionals_passed = if params.flags.has_opt() != 0 {

zjit/src/hir.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2228,24 +2228,23 @@ pub enum ValidationError {
22282228
}
22292229

22302230
/// Check if we can do a direct send to the given iseq with the given args.
2231-
fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq_t, ci: *const rb_callinfo, send_insn: InsnId, args: &[InsnId], blockiseq: Option<IseqPtr>) -> bool {
2231+
fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq_t, ci: *const rb_callinfo, send_insn: InsnId, args: &[InsnId]) -> bool {
22322232
let mut can_send = true;
22332233
let mut count_failure = |counter| {
22342234
can_send = false;
22352235
function.count(block, counter);
22362236
};
22372237
let params = unsafe { iseq.params() };
22382238

2239-
let caller_has_literal_block: bool = blockiseq.is_some();
22402239
let callee_has_block_param = 0 != params.flags.has_block();
2240+
let caller_passes_block_arg = (unsafe { rb_vm_ci_flag(ci) } & VM_CALL_ARGS_BLOCKARG) != 0;
22412241

22422242
use Counter::*;
22432243
if 0 != params.flags.has_rest() { count_failure(complex_arg_pass_param_rest) }
22442244
if 0 != params.flags.has_post() { count_failure(complex_arg_pass_param_post) }
2245-
if callee_has_block_param && !caller_has_literal_block
2246-
{ count_failure(complex_arg_pass_param_block) }
22472245
if 0 != params.flags.forwardable() { count_failure(complex_arg_pass_param_forwardable) }
2248-
2246+
if callee_has_block_param && caller_passes_block_arg
2247+
{ count_failure(complex_arg_pass_param_block) }
22492248
if 0 != params.flags.has_kwrest() { count_failure(complex_arg_pass_param_kwrest) }
22502249

22512250
if !can_send {
@@ -3570,7 +3569,7 @@ impl Function {
35703569
// Only specialize positional-positional calls
35713570
// TODO(max): Handle other kinds of parameter passing
35723571
let iseq = unsafe { get_def_iseq_ptr((*cme).def) };
3573-
if !can_direct_send(self, block, iseq, ci, insn_id, args.as_slice(), blockiseq) {
3572+
if !can_direct_send(self, block, iseq, ci, insn_id, args.as_slice()) {
35743573
self.push_insn_id(block, insn_id); continue;
35753574
}
35763575

@@ -3609,7 +3608,7 @@ impl Function {
36093608
let capture = unsafe { proc_block.as_.captured.as_ref() };
36103609
let iseq = unsafe { *capture.code.iseq.as_ref() };
36113610

3612-
if !can_direct_send(self, block, iseq, ci, insn_id, args.as_slice(), None) {
3611+
if !can_direct_send(self, block, iseq, ci, insn_id, args.as_slice()) {
36133612
self.push_insn_id(block, insn_id); continue;
36143613
}
36153614

@@ -3976,7 +3975,7 @@ impl Function {
39763975
// If not, we can't do direct dispatch.
39773976
let super_iseq = unsafe { get_def_iseq_ptr((*super_cme).def) };
39783977
// TODO: pass Option<blockiseq> to can_direct_send when we start specializing `super { ... }`.
3979-
if !can_direct_send(self, block, super_iseq, ci, insn_id, args.as_slice(), None) {
3978+
if !can_direct_send(self, block, super_iseq, ci, insn_id, args.as_slice()) {
39803979
self.push_insn_id(block, insn_id);
39813980
self.set_dynamic_send_reason(insn_id, SuperTargetComplexArgsPass);
39823981
continue;

zjit/src/hir/opt_tests.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4514,7 +4514,10 @@ mod hir_opt_tests {
45144514
v12:NilClass = Const Value(nil)
45154515
PatchPoint MethodRedefined(Hash@0x1008, new@0x1009, cme:0x1010)
45164516
v46:HashExact = ObjectAllocClass Hash:VALUE(0x1008)
4517-
v19:BasicObject = Send v46, :initialize # SendFallbackReason: Complex argument passing
4517+
v47:Fixnum[0] = Const Value(0)
4518+
PatchPoint NoSingletonClass(Hash@0x1008)
4519+
PatchPoint MethodRedefined(Hash@0x1008, initialize@0x1038, cme:0x1040)
4520+
v51:BasicObject = SendDirect v46, 0x1068, :initialize (0x1078), v47
45184521
CheckInterrupts
45194522
Return v46
45204523
");
@@ -7865,6 +7868,67 @@ mod hir_opt_tests {
78657868
");
78667869
}
78677870

7871+
#[test]
7872+
fn test_send_iseq_with_block_param_no_block() {
7873+
let result = eval("
7874+
def foo(&blk)
7875+
blk ? blk.call : 42
7876+
end
7877+
def test = foo
7878+
test
7879+
test
7880+
");
7881+
assert_eq!(VALUE::fixnum_from_usize(42), result);
7882+
assert_snapshot!(hir_string("test"), @"
7883+
fn test@<compiled>:5:
7884+
bb1():
7885+
EntryPoint interpreter
7886+
v1:BasicObject = LoadSelf
7887+
Jump bb3(v1)
7888+
bb2():
7889+
EntryPoint JIT(0)
7890+
v4:BasicObject = LoadArg :self@0
7891+
Jump bb3(v4)
7892+
bb3(v6:BasicObject):
7893+
PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010)
7894+
v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)]
7895+
v19:BasicObject = SendDirect v18, 0x1038, :foo (0x1048)
7896+
CheckInterrupts
7897+
Return v19
7898+
");
7899+
}
7900+
7901+
#[test]
7902+
fn test_send_bmethod_with_block_param_no_block() {
7903+
let result = eval("
7904+
define_method(:foo) { |&blk|
7905+
blk ? blk.call : 42
7906+
}
7907+
def test = foo
7908+
test
7909+
test
7910+
");
7911+
assert_eq!(VALUE::fixnum_from_usize(42), result);
7912+
assert_snapshot!(hir_string("test"), @"
7913+
fn test@<compiled>:5:
7914+
bb1():
7915+
EntryPoint interpreter
7916+
v1:BasicObject = LoadSelf
7917+
Jump bb3(v1)
7918+
bb2():
7919+
EntryPoint JIT(0)
7920+
v4:BasicObject = LoadArg :self@0
7921+
Jump bb3(v4)
7922+
bb3(v6:BasicObject):
7923+
PatchPoint SingleRactorMode
7924+
PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010)
7925+
v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)]
7926+
v20:BasicObject = SendDirect v19, 0x1038, :foo (0x1048)
7927+
CheckInterrupts
7928+
Return v20
7929+
");
7930+
}
7931+
78687932
#[test]
78697933
fn test_inline_attr_reader_constant() {
78707934
eval("

0 commit comments

Comments
 (0)