Introduce Allocationless Anonymous Splat Forwarding

Ruby makes it easy to delegate all arguments from one method to another:

```ruby
def f(*args, **kw)
  g(*args, **kw)
end
```

Unfortunately, this indirection decreases performance.  One reason it
decreases performance is that this allocates an array and a hash per
call to `f`, even if `args` and `kw` are not modified.

Due to Ruby's ability to modify almost anything at runtime, it's
difficult to avoid the array allocation in the general case. For
example, it's not safe to avoid the allocation in a case like this:

```ruby
def f(*args, **kw)
  foo(bar)
  g(*args, **kw)
end
```

Because `foo` may be `eval` and `bar` may be a string referencing `args`
or `kw`.

To fix this correctly, you need to perform something similar to escape
analysis on the variables.  However, there is a case where you can
avoid the allocation without doing escape analysis, and that is when
the splat variables are anonymous:

```ruby
def f(*, **)
  g(*, **)
end
```

When splat variables are anonymous, it is not possible to reference
them directly, it is only possible to use them as splats to other
methods.  Since that is the case, if `f` is called with a regular
splat and a keyword splat, it can pass the arguments directly to
`g` without copying them, avoiding allocation.  For example:

```ruby
def g(a, b:)
  a + b
end

def f(*, **)
  g(*, **)
end

a = [1]
kw = {b: 2}

f(*a, **kw)
```

I call this technique: Allocationless Anonymous Splat Forwarding.

This is implemented using a couple additional iseq param flags,
anon_rest and anon_kwrest.  If anon_rest is set, and an array splat
is passed when calling the method when the array splat can be used
without modification, `setup_parameters_complex` does not duplicate
it.  Similarly, if anon_kwest is set, and a keyword splat is passed
when calling the method, `setup_parameters_complex` does not
duplicate it.
This commit is contained in:
Jeremy Evans 2023-11-30 14:58:42 -08:00
Родитель b8516d6d01
Коммит 0f90a24a81
4 изменённых файлов: 36 добавлений и 4 удалений

Просмотреть файл

@ -2072,6 +2072,7 @@ iseq_set_arguments(rb_iseq_t *iseq, LINK_ANCHOR *const optargs, const NODE *cons
if (rest_id) {
body->param.rest_start = arg_size++;
body->param.flags.has_rest = TRUE;
if (rest_id == '*') body->param.flags.anon_rest = TRUE;
assert(body->param.rest_start != -1);
}
@ -2090,10 +2091,15 @@ iseq_set_arguments(rb_iseq_t *iseq, LINK_ANCHOR *const optargs, const NODE *cons
arg_size = iseq_set_arguments_keywords(iseq, optargs, args, arg_size);
}
else if (args->kw_rest_arg) {
ID kw_id = iseq->body->local_table[arg_size];
struct rb_iseq_param_keyword *keyword = ZALLOC_N(struct rb_iseq_param_keyword, 1);
keyword->rest_start = arg_size++;
body->param.keyword = keyword;
body->param.flags.has_kwrest = TRUE;
static ID anon_kwrest = 0;
if (!anon_kwrest) anon_kwrest = rb_intern("**");
if (kw_id == anon_kwrest) body->param.flags.anon_kwrest = TRUE;
}
else if (args->no_kwarg) {
body->param.flags.accepts_no_kwarg = TRUE;
@ -12848,6 +12854,8 @@ ibf_load_iseq_each(struct ibf_load *load, rb_iseq_t *iseq, ibf_offset_t offset)
load_body->param.flags.ambiguous_param0 = (param_flags >> 7) & 1;
load_body->param.flags.accepts_no_kwarg = (param_flags >> 8) & 1;
load_body->param.flags.ruby2_keywords = (param_flags >> 9) & 1;
load_body->param.flags.anon_rest = (param_flags >> 10) & 1;
load_body->param.flags.anon_kwrest = (param_flags >> 11) & 1;
load_body->param.size = param_size;
load_body->param.lead_num = param_lead_num;
load_body->param.opt_num = param_opt_num;

Просмотреть файл

@ -1131,6 +1131,8 @@ module RubyVM::RJIT # :nodoc: all
ambiguous_param0: [CType::BitField.new(1, 7), 7],
accepts_no_kwarg: [CType::BitField.new(1, 0), 8],
ruby2_keywords: [CType::BitField.new(1, 1), 9],
anon_rest: [CType::BitField.new(1, 2), 10],
anon_kwrest: [CType::BitField.new(1, 3), 11],
), Primitive.cexpr!("OFFSETOF(((struct rb_iseq_constant_body *)NULL)->param, flags)")],
size: [CType::Immediate.parse("unsigned int"), Primitive.cexpr!("OFFSETOF(((struct rb_iseq_constant_body *)NULL)->param, size)")],
lead_num: [CType::Immediate.parse("int"), Primitive.cexpr!("OFFSETOF(((struct rb_iseq_constant_body *)NULL)->param, lead_num)")],

Просмотреть файл

@ -475,7 +475,8 @@ setup_parameters_complex(rb_execution_context_t * const ec, const rb_iseq_t * co
const int min_argc = ISEQ_BODY(iseq)->param.lead_num + ISEQ_BODY(iseq)->param.post_num;
const int max_argc = (ISEQ_BODY(iseq)->param.flags.has_rest == FALSE) ? min_argc + ISEQ_BODY(iseq)->param.opt_num : UNLIMITED_ARGUMENTS;
int given_argc;
unsigned int kw_flag = vm_ci_flag(ci) & (VM_CALL_KWARG | VM_CALL_KW_SPLAT | VM_CALL_KW_SPLAT_MUT);
unsigned int ci_flag = vm_ci_flag(ci);
unsigned int kw_flag = ci_flag & (VM_CALL_KWARG | VM_CALL_KW_SPLAT | VM_CALL_KW_SPLAT_MUT);
int opt_pc = 0, allow_autosplat = !kw_flag;
struct args_info args_body, *args;
VALUE keyword_hash = Qnil;
@ -510,7 +511,26 @@ setup_parameters_complex(rb_execution_context_t * const ec, const rb_iseq_t * co
args = &args_body;
given_argc = args->argc = calling->argc;
args->argv = locals;
args->rest_dupped = vm_ci_flag(ci) & VM_CALL_ARGS_SPLAT_MUT;
args->rest_dupped = ci_flag & VM_CALL_ARGS_SPLAT_MUT;
if (UNLIKELY(ISEQ_BODY(iseq)->param.flags.anon_rest)) {
if ((ci_flag & VM_CALL_ARGS_SPLAT) &&
given_argc == ISEQ_BODY(iseq)->param.lead_num + (kw_flag ? 2 : 1) &&
!ISEQ_BODY(iseq)->param.flags.has_opt &&
!ISEQ_BODY(iseq)->param.flags.has_post &&
(!kw_flag ||
!ISEQ_BODY(iseq)->param.flags.has_kw ||
!ISEQ_BODY(iseq)->param.flags.has_kwrest ||
!ISEQ_BODY(iseq)->param.flags.accepts_no_kwarg)) {
args->rest_dupped = true;
}
}
if (UNLIKELY(ISEQ_BODY(iseq)->param.flags.anon_kwrest)) {
if (kw_flag & VM_CALL_KW_SPLAT) {
kw_flag |= VM_CALL_KW_SPLAT_MUT;
}
}
if (kw_flag & VM_CALL_KWARG) {
args->kw_arg = vm_ci_kwarg(ci);
@ -534,7 +554,7 @@ setup_parameters_complex(rb_execution_context_t * const ec, const rb_iseq_t * co
args->kw_argv = NULL;
}
if ((vm_ci_flag(ci) & VM_CALL_ARGS_SPLAT) && (vm_ci_flag(ci) & VM_CALL_KW_SPLAT)) {
if ((ci_flag & VM_CALL_ARGS_SPLAT) && (ci_flag & VM_CALL_KW_SPLAT)) {
// f(*a, **kw)
args->rest_index = 0;
keyword_hash = locals[--args->argc];
@ -563,7 +583,7 @@ setup_parameters_complex(rb_execution_context_t * const ec, const rb_iseq_t * co
int len = RARRAY_LENINT(args->rest);
given_argc += len - 2;
}
else if (vm_ci_flag(ci) & VM_CALL_ARGS_SPLAT) {
else if (ci_flag & VM_CALL_ARGS_SPLAT) {
// f(*a)
args->rest_index = 0;
args->rest = locals[--args->argc];

Просмотреть файл

@ -416,6 +416,8 @@ struct rb_iseq_constant_body {
unsigned int ambiguous_param0 : 1; /* {|a|} */
unsigned int accepts_no_kwarg : 1;
unsigned int ruby2_keywords: 1;
unsigned int anon_rest: 1;
unsigned int anon_kwrest: 1;
} flags;
unsigned int size;