[前][次][番号順一覧][スレッド一覧]

ruby-changes:60434

From: Jeremy <ko1@a...>
Date: Wed, 18 Mar 2020 04:10:13 +0900 (JST)
Subject: [ruby-changes:60434] d2c41b1bff (master): Reduce allocations for keyword argument hashes

https://git.ruby-lang.org/ruby.git/commit/?id=d2c41b1bff

From d2c41b1bff1f3102544bb0d03d4e82356d034d33 Mon Sep 17 00:00:00 2001
From: Jeremy Evans <code@j...>
Date: Mon, 24 Feb 2020 12:05:07 -0800
Subject: Reduce allocations for keyword argument hashes

Previously, passing a keyword splat to a method always allocated
a hash on the caller side, and accepting arbitrary keywords in
a method allocated a separate hash on the callee side.  Passing
explicit keywords to a method that accepted a keyword splat
did not allocate a hash on the caller side, but resulted in two
hashes allocated on the callee side.

This commit makes passing a single keyword splat to a method not
allocate a hash on the caller side.  Passing multiple keyword
splats or a mix of explicit keywords and a keyword splat still
generates a hash on the caller side.  On the callee side,
if arbitrary keywords are not accepted, it does not allocate a
hash.  If arbitrary keywords are accepted, it will allocate a
hash, but this commit uses a callinfo flag to indicate whether
the caller already allocated a hash, and if so, the callee can
use the passed hash without duplicating it.  So this commit
should make it so that a maximum of a single hash is allocated
during method calls.

To set the callinfo flag appropriately, method call argument
compilation checks if only a single keyword splat is given.
If only one keyword splat is given, the VM_CALL_KW_SPLAT_MUT
callinfo flag is not set, since in that case the keyword
splat is passed directly and not mutable.  If more than one
splat is used, a new hash needs to be generated on the caller
side, and in that case the callinfo flag is set, indicating
the keyword splat is mutable by the callee.

In compile_hash, used for both hash and keyword argument
compilation, if compiling keyword arguments and only a
single keyword splat is used, pass the argument directly.

On the caller side, in vm_args.c, the callinfo flag needs to
be recognized and handled.  Because the keyword splat
argument may not be a hash, it needs to be converted to a
hash first if not.  Then, unless the callinfo flag is set,
the hash needs to be duplicated.  The temporary copy of the
callinfo flag, kw_flag, is updated if a hash was duplicated,
to prevent the need to duplicate it again.  If we are
converting to a hash or duplicating a hash, we need to update
the argument array, which can including duplicating the
positional splat array if one was passed.  CALLER_SETUP_ARG
and a couple other places needs to be modified to handle
similar issues for other types of calls.

This includes fairly comprehensive tests for different ways
keywords are handled internally, checking that you get equal
results but that keyword splats on the caller side result in
distinct objects for keyword rest parameters.

Included are benchmarks for keyword argument calls.
Brief results when compiled without optimization:

  def kw(a: 1) a end
  def kws(**kw) kw end
  h = {a: 1}

  kw(a: 1)       # about same
  kw(**h)        # 2.37x faster
  kws(a: 1)      # 1.30x faster
  kws(**h)       # 2.19x faster
  kw(a: 1, **h)  # 1.03x slower
  kw(**h, **h)   # about same
  kws(a: 1, **h) # 1.16x faster
  kws(**h, **h)  # 1.14x faster

diff --git a/NEWS.md b/NEWS.md
index 3fabf7b..7b7427d 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -139,6 +139,11 @@ Excluding feature bug fixes. https://github.com/ruby/ruby/blob/trunk/NEWS.md#L139
 
 ## Implementation improvements
 
+* The number of hashes allocated when using a keyword splat in
+  a method call has been reduced to a maximum of 1, and passing
+  a keyword splat to a method that accepts specific keywords
+  does not allocate a hash.
+
 ## Miscellaneous changes
 
 * Methods using `ruby2_keywords` will no longer keep empty keyword
diff --git a/benchmark/keyword_arguments.yml b/benchmark/keyword_arguments.yml
new file mode 100644
index 0000000..fce6bce
--- /dev/null
+++ b/benchmark/keyword_arguments.yml
@@ -0,0 +1,13 @@ https://github.com/ruby/ruby/blob/trunk/benchmark/keyword_arguments.yml#L1
+prelude: |
+  h = {a: 1}
+  def kw(a: 1) a end
+  def kws(**kw) kw end
+benchmark:
+  kw_to_kw: "kw(a: 1)"
+  kw_splat_to_kw: "kw(**h)"
+  kw_to_kw_splat: "kws(a: 1)"
+  kw_splat_to_kw_splat: "kws(**h)"
+  kw_and_splat_to_kw: "kw(a: 1, **h)"
+  kw_splats_to_kw: "kw(**h, **h)"
+  kw_and_splat_to_kw_splat: "kws(a: 1, **h)"
+  kw_splats_to_kw_splat: "kws(**h, **h)"
diff --git a/compile.c b/compile.c
index f55c76f..efca717 100644
--- a/compile.c
+++ b/compile.c
@@ -3918,20 +3918,27 @@ compile_keyword_arg(rb_iseq_t *iseq, LINK_ANCHOR *const ret, https://github.com/ruby/ruby/blob/trunk/compile.c#L3918
 
     if (root_node->nd_head && nd_type(root_node->nd_head) == NODE_LIST) {
 	const NODE *node = root_node->nd_head;
+        int seen_nodes = 0;
 
 	while (node) {
 	    const NODE *key_node = node->nd_head;
+            seen_nodes++;
 
 	    assert(nd_type(node) == NODE_LIST);
-	    if (!key_node) {
-		if (flag) *flag |= VM_CALL_KW_SPLAT;
-		return FALSE;
-	    }
-	    else if (nd_type(key_node) == NODE_LIT && RB_TYPE_P(key_node->nd_lit, T_SYMBOL)) {
+            if (key_node && nd_type(key_node) == NODE_LIT && RB_TYPE_P(key_node->nd_lit, T_SYMBOL)) {
 		/* can be keywords */
 	    }
 	    else {
-		if (flag) *flag |= VM_CALL_KW_SPLAT;
+                if (flag) {
+                    *flag |= VM_CALL_KW_SPLAT;
+                    if (seen_nodes > 1 || node->nd_next->nd_next) {
+                        /* A new hash will be created for the keyword arguments
+                         * in this case, so mark the method as passing mutable
+                         * keyword splat.
+                         */
+                        *flag |= VM_CALL_KW_SPLAT_MUT;
+                    }
+                }
 		return FALSE;
 	    }
 	    node = node->nd_next; /* skip value node */
@@ -4312,31 +4319,47 @@ compile_hash(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, int popp https://github.com/ruby/ruby/blob/trunk/compile.c#L4319
 
                 if (empty_kw) {
                     if (only_kw && method_call_keywords) {
-                        /* **{} appears at the last, so it won't be modified.
+                        /* **{} appears at the only keyword argument in method call,
+                         * so it won't be modified.
                          * kw is a special NODE_LIT that contains a special empty hash,
-                         * so this emits: putobject {}
+                         * so this emits: putobject {}.
+                         * This is only done for method calls and not for literal hashes,
+                         * because literal hashes should always result in a new hash.
                          */
                         NO_CHECK(COMPILE(ret, "keyword splat", kw));
                     }
                     else if (first_kw) {
-                        /* **{} appears at the first, so it may be modified.
+                        /* **{} appears as the first keyword argument, so it may be modified.
                          * We need to create a fresh hash object.
                          */
                         ADD_INSN1(ret, line, newhash, INT2FIX(0));
                     }
+                    /* Any empty keyword splats that are not the first can be ignored.
+                     * since merging an empty hash into the existing hash is the same
+                     * as not merging it. */
                 }
                 else {
-                    /* This is not empty hash: **{k:1}.
-                     * We need to clone the hash (if first), or merge the hash to
-                     * the accumulated hash (if not first).
-                     */
-                    ADD_INSN1(ret, line, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE));
-                    if (first_kw) ADD_INSN1(ret, line, newhash, INT2FIX(0));
-                    else ADD_INSN(ret, line, swap);
+                    if (only_kw && method_call_keywords) {
+                        /* **kw is only keyword argument in method call.
+                         * Use directly.  This will be not be flagged as mutable.
+                         * This is only done for method calls and not for literal hashes,
+                         * because literal hashes should always result in a new hash.
+                         */
+                        NO_CHECK(COMPILE(ret, "keyword splat", kw));
+                    }
+                    else {
+                        /* There is more than one keyword argument, or this is not a method
+                         * call.  In that case, we need to add an empty hash (if first keyword),
+                         * or merge the hash to the accumulated hash (if not the first keyword).
+                         */
+                        ADD_INSN1(ret, line, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE));
+                        if (first_kw) ADD_INSN1(ret, line, newhash, INT2FIX(0));
+                        else ADD_INSN(ret, line, swap);
 
-                    NO_CHECK(COMPILE(ret, "keyword splat", kw));
+                        NO_CHECK(COMPILE(ret, "keyword splat", kw));
 
-                    ADD_SEND(ret, line, id_core_hash_merge_kwd, INT2FIX(2));
+                        ADD_SEND(ret, line, id_core_hash_merge_kwd, INT2FIX(2));
+                    }
                 }
 
                 first_chunk = 0;
@@ -7808,10 +7831,10 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, in https://github.com/ruby/ruby/blob/trunk/compile.c#L7831
 		if (local_body->param.flags.has_kwrest) {
 		    int idx = local_body->local_table_size - local_kwd->rest_start;
 		    ADD_GETLOCAL(args, line, idx, lvar_level);
-		    ADD_SEND (args, line, rb_intern("dup"), INT2FIX(0));
 		}
 		else {
 		    ADD_INSN1(args, line, newhash, INT2FIX(0));
+                    flag |= VM_CALL_KW_SPLAT_MUT;
 		}
 		for (i = 0; i < local_kwd->num; ++i) {
 		    ID id = local_kwd->table[i];
@@ -7831,7 +7854,6 @@ iseq_compile_each0(rb_iseq_t * (... truncated)

--
ML: ruby-changes@q...
Info: http://www.atdot.net/~ko1/quickml/

[前][次][番号順一覧][スレッド一覧]