aboutsummaryrefslogtreecommitdiffstats
path: root/vm_args.c
diff options
context:
space:
mode:
authorJeremy Evans <code@jeremyevans.net>2023-11-30 14:58:42 -0800
committerJeremy Evans <code@jeremyevans.net>2024-01-24 18:25:55 -0800
commit0f90a24a816bec438edb272fb83f334484dfc285 (patch)
treec753fc56c52aa777488c69da7f1b21175d331832 /vm_args.c
parentb8516d6d0174a10579817f4bcf5a94c8ef03dd7a (diff)
downloadruby-0f90a24a816bec438edb272fb83f334484dfc285.tar.gz
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.
Diffstat (limited to 'vm_args.c')
-rw-r--r--vm_args.c28
1 files changed, 24 insertions, 4 deletions
diff --git a/vm_args.c b/vm_args.c
index 8171dcb8e0..e7043ac897 100644
--- a/vm_args.c
+++ b/vm_args.c
@@ -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];