Skip to content

Commit 703eee7

Browse files
authored
YJIT: Drop extra arguments passed by yield (#9596)
Support dropping extra arguments passed by `yield` in blocks. For example `10.times { work }` drops the count argument. This is common enough that it's about 3% of fallback reasons in `lobsters`. Only support simple cases where the surplus arguments are at the top of the stack, that way they just need to be popped, which takes no work.
1 parent c7e87b2 commit 703eee7

File tree

3 files changed

+101
-37
lines changed

3 files changed

+101
-37
lines changed

bootstraptest/test_yjit.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,42 @@
1+
# test discarding extra yield arguments
2+
assert_equal "2210150001501015", %q{
3+
def splat_kw(ary) = yield *ary, a: 1
4+
5+
def splat(ary) = yield *ary
6+
7+
def kw = yield 1, 2, a: 0
8+
9+
def simple = yield 0, 1
10+
11+
def calls
12+
[
13+
splat([1, 1, 2]) { |x, y| x + y },
14+
splat([1, 1, 2]) { |y, opt = raise| opt + y},
15+
splat_kw([0, 1]) { |a:| a },
16+
kw { |a:| a },
17+
kw { |a| a },
18+
simple { 5.itself },
19+
simple { |a| a },
20+
simple { |opt = raise| opt },
21+
simple { |*rest| rest },
22+
simple { |opt_kw: 5| opt_kw },
23+
# autosplat ineractions
24+
[0, 1, 2].yield_self { |a, b| [a, b] },
25+
[0, 1, 2].yield_self { |a, opt = raise| [a, opt] },
26+
[1].yield_self { |a, opt = 4| a + opt },
27+
]
28+
end
29+
30+
calls.join
31+
}
32+
33+
# test autosplat with empty splat
34+
assert_equal "ok", %q{
35+
def m(pos, splat) = yield pos, *splat
36+
37+
m([:ok], []) {|v0,| v0 }
38+
}
39+
140
# regression test for send stack shifting
241
assert_normal_exit %q{
342
def foo(a, b)

yjit/src/codegen.rs

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6117,6 +6117,7 @@ fn gen_send_iseq(
61176117
let supplying_kws = unsafe { vm_ci_flag(ci) & VM_CALL_KWARG } != 0;
61186118
let iseq_has_rest = unsafe { get_iseq_flags_has_rest(iseq) };
61196119
let iseq_has_block_param = unsafe { get_iseq_flags_has_block(iseq) };
6120+
let arg_setup_block = captured_opnd.is_some(); // arg_setup_type: arg_setup_block (invokeblock)
61206121

61216122
// For computing offsets to callee locals
61226123
let num_params = unsafe { get_iseq_body_param_size(iseq) };
@@ -6137,10 +6138,10 @@ fn gen_send_iseq(
61376138
// Arity handling and optional parameter setup
61386139
let mut opts_filled = argc - required_num - kw_arg_num;
61396140
let opt_num = unsafe { get_iseq_body_param_opt_num(iseq) };
6140-
// We have a rest parameter so there could be more args
6141-
// than are required + optional. Those will go in rest.
6141+
// With a rest parameter or a yield to a block,
6142+
// callers can pass more than required + optional.
61426143
// So we cap ops_filled at opt_num.
6143-
if iseq_has_rest {
6144+
if iseq_has_rest || arg_setup_block {
61446145
opts_filled = min(opts_filled, opt_num);
61456146
}
61466147
let mut opts_missing: i32 = opt_num - opts_filled;
@@ -6159,11 +6160,17 @@ fn gen_send_iseq(
61596160
exit_if_supplying_kws_and_accept_no_kwargs(asm, supplying_kws, iseq)?;
61606161
exit_if_splat_and_zsuper(asm, flags)?;
61616162
exit_if_doing_kw_and_splat(asm, doing_kw_call, flags)?;
6162-
exit_if_wrong_number_arguments(asm, opts_filled, flags, opt_num, iseq_has_rest)?;
6163+
exit_if_wrong_number_arguments(asm, arg_setup_block, opts_filled, flags, opt_num, iseq_has_rest)?;
61636164
exit_if_doing_kw_and_opts_missing(asm, doing_kw_call, opts_missing)?;
61646165
exit_if_has_rest_and_optional_and_block(asm, iseq_has_rest, opt_num, iseq, block_arg)?;
61656166
let block_arg_type = exit_if_unsupported_block_arg_type(jit, asm, block_arg)?;
61666167

6168+
// Bail if we can't drop extra arguments for a yield by just popping them
6169+
if supplying_kws && arg_setup_block && argc > (kw_arg_num + required_num + opt_num) {
6170+
gen_counter_incr(asm, Counter::send_iseq_complex_discard_extras);
6171+
return None;
6172+
}
6173+
61676174
// Block parameter handling. This mirrors setup_parameters_complex().
61686175
if iseq_has_block_param {
61696176
if unsafe { get_iseq_body_local_iseq(iseq) == iseq } {
@@ -6249,35 +6256,6 @@ fn gen_send_iseq(
62496256
}
62506257
}
62516258

6252-
// Check if we need the arg0 splat handling of vm_callee_setup_block_arg()
6253-
// Also known as "autosplat" inside setup_parameters_complex()
6254-
let arg_setup_block = captured_opnd.is_some(); // arg_setup_type: arg_setup_block (invokeblock)
6255-
let block_arg0_splat = arg_setup_block && argc == 1 && unsafe {
6256-
(get_iseq_flags_has_lead(iseq) || opt_num > 1)
6257-
&& !get_iseq_flags_ambiguous_param0(iseq)
6258-
};
6259-
if block_arg0_splat {
6260-
// If block_arg0_splat, we still need side exits after splat, but
6261-
// doing push_splat_args here disallows it. So bail out.
6262-
if flags & VM_CALL_ARGS_SPLAT != 0 && !iseq_has_rest {
6263-
gen_counter_incr(asm, Counter::invokeblock_iseq_arg0_args_splat);
6264-
return None;
6265-
}
6266-
// The block_arg0_splat implementation is for the rb_simple_iseq_p case,
6267-
// but doing_kw_call means it's not a simple ISEQ.
6268-
if doing_kw_call {
6269-
gen_counter_incr(asm, Counter::invokeblock_iseq_arg0_has_kw);
6270-
return None;
6271-
}
6272-
// The block_arg0_splat implementation cannot deal with optional parameters.
6273-
// This is a setup_parameters_complex() situation and interacts with the
6274-
// starting position of the callee.
6275-
if opt_num > 1 {
6276-
gen_counter_incr(asm, Counter::invokeblock_iseq_arg0_optional);
6277-
return None;
6278-
}
6279-
}
6280-
62816259
let splat_array_length = if flags & VM_CALL_ARGS_SPLAT != 0 {
62826260
let array = jit.peek_at_stack(&asm.ctx, if block_arg { 1 } else { 0 }) ;
62836261
let array_length = if array == Qnil {
@@ -6318,6 +6296,33 @@ fn gen_send_iseq(
63186296
None
63196297
};
63206298

6299+
// Check if we need the arg0 splat handling of vm_callee_setup_block_arg()
6300+
// Also known as "autosplat" inside setup_parameters_complex().
6301+
// Autosplat checks argc == 1 after splat and kwsplat processing, so make
6302+
// sure to amend this if we start support kw_splat.
6303+
let block_arg0_splat = arg_setup_block
6304+
&& (argc == 1 || (argc == 2 && splat_array_length == Some(0)))
6305+
&& !supplying_kws && !doing_kw_call
6306+
&& unsafe {
6307+
(get_iseq_flags_has_lead(iseq) || opt_num > 1)
6308+
&& !get_iseq_flags_ambiguous_param0(iseq)
6309+
};
6310+
if block_arg0_splat {
6311+
// If block_arg0_splat, we still need side exits after splat, but
6312+
// the splat modifies the stack which breaks side exits. So bail out.
6313+
if flags & VM_CALL_ARGS_SPLAT != 0 {
6314+
gen_counter_incr(asm, Counter::invokeblock_iseq_arg0_args_splat);
6315+
return None;
6316+
}
6317+
// The block_arg0_splat implementation cannot deal with optional parameters.
6318+
// This is a setup_parameters_complex() situation and interacts with the
6319+
// starting position of the callee.
6320+
if opt_num > 1 {
6321+
gen_counter_incr(asm, Counter::invokeblock_iseq_arg0_optional);
6322+
return None;
6323+
}
6324+
}
6325+
63216326
// Adjust `opts_filled` and `opts_missing` taking
63226327
// into account the size of the splat expansion.
63236328
if let Some(len) = splat_array_length {
@@ -6605,6 +6610,19 @@ fn gen_send_iseq(
66056610
asm.store(rest_param, rest_param_array);
66066611
}
66076612

6613+
// Pop surplus positional arguments when yielding
6614+
if arg_setup_block {
6615+
let extras = argc - required_num - opt_num;
6616+
if extras > 0 {
6617+
// Checked earlier. If there are keyword args, then
6618+
// the positional arguments are not at the stack top.
6619+
assert_eq!(0, kw_arg_num);
6620+
6621+
asm.stack_pop(extras as usize);
6622+
argc = required_num + opt_num;
6623+
}
6624+
}
6625+
66086626
if doing_kw_call {
66096627
// Here we're calling a method with keyword arguments and specifying
66106628
// keyword arguments at this call site.
@@ -7034,11 +7052,18 @@ fn exit_if_doing_kw_and_splat(asm: &mut Assembler, doing_kw_call: bool, flags: u
70347052
}
70357053

70367054
#[must_use]
7037-
fn exit_if_wrong_number_arguments(asm: &mut Assembler, opts_filled: i32, flags: u32, opt_num: i32, iseq_has_rest: bool) -> Option<()> {
7055+
fn exit_if_wrong_number_arguments(
7056+
asm: &mut Assembler,
7057+
args_setup_block: bool,
7058+
opts_filled: i32,
7059+
flags: u32,
7060+
opt_num: i32,
7061+
iseq_has_rest: bool,
7062+
) -> Option<()> {
70387063
// Too few arguments and no splat to make up for it
70397064
let too_few = opts_filled < 0 && flags & VM_CALL_ARGS_SPLAT == 0;
7040-
// Too many arguments and no place to put them (i.e. rest arg)
7041-
let too_many = opts_filled > opt_num && !iseq_has_rest;
7065+
// Too many arguments and no sink that take them
7066+
let too_many = opts_filled > opt_num && !(iseq_has_rest || args_setup_block);
70427067

70437068
exit_if(asm, too_few || too_many, Counter::send_iseq_arity_error)
70447069
}

yjit/src/stats.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ make_counters! {
331331
send_iseq_arity_error,
332332
send_iseq_block_arg_type,
333333
send_iseq_clobbering_block_arg,
334+
send_iseq_complex_discard_extras,
334335
send_iseq_leaf_builtin_block_arg_block_param,
335336
send_iseq_only_keywords,
336337
send_iseq_kw_splat,
@@ -391,7 +392,6 @@ make_counters! {
391392
invokeblock_megamorphic,
392393
invokeblock_none,
393394
invokeblock_iseq_arg0_optional,
394-
invokeblock_iseq_arg0_has_kw,
395395
invokeblock_iseq_arg0_args_splat,
396396
invokeblock_iseq_arg0_not_array,
397397
invokeblock_iseq_arg0_wrong_len,

0 commit comments

Comments
 (0)