ruby-changes:69814
From: Jeremy <ko1@a...>
Date: Fri, 19 Nov 2021 08:10:37 +0900 (JST)
Subject: [ruby-changes:69814] b08dacfea3 (master): Optimize dynamic string interpolation for symbol/true/false/nil/0-9
https://git.ruby-lang.org/ruby.git/commit/?id=b08dacfea3 From b08dacfea39ad8da3f1fd7fdd0e4538cc892ec44 Mon Sep 17 00:00:00 2001 From: Jeremy Evans <code@j...> Date: Thu, 18 Nov 2021 15:10:20 -0800 Subject: Optimize dynamic string interpolation for symbol/true/false/nil/0-9 This provides a significant speedup for symbol, true, false, nil, and 0-9, class/module, and a small speedup in most other cases. Speedups (using included benchmarks): :symbol :: 60% 0-9 :: 50% Class/Module :: 50% nil/true/false :: 20% integer :: 10% [] :: 10% "" :: 3% One reason this approach is faster is it reduces the number of VM instructions for each interpolated value. Initial idea, approach, and benchmarks from Eric Wong. I applied the same approach against the master branch, updating it to handle the significant internal changes since this was first proposed 4 years ago (such as CALL_INFO/CALL_CACHE -> CALL_DATA). I also expanded it to optimize true/false/nil/0-9/class/module, and added handling of missing methods, refined methods, and RUBY_DEBUG. This renames the tostring insn to anytostring, and adds an objtostring insn that implements the optimization. This requires making a few functions non-static, and adding some non-static functions. This disables 4 YJIT tests. Those tests should be reenabled after YJIT optimizes the new objtostring insn. Implements [Feature #13715] Co-authored-by: Eric Wong <e@8...> Co-authored-by: Alan Wu <XrXr@u...> Co-authored-by: Yusuke Endoh <mame@r...> Co-authored-by: Koichi Sasada <ko1@a...> --- benchmark/vm_dstr_ary.rb | 6 ++++ benchmark/vm_dstr_bool.rb | 7 +++++ benchmark/vm_dstr_class_module.rb | 10 ++++++ benchmark/vm_dstr_digit.rb | 7 +++++ benchmark/vm_dstr_int.rb | 5 +++ benchmark/vm_dstr_nil.rb | 6 ++++ benchmark/vm_dstr_obj.rb | 6 ++++ benchmark/vm_dstr_obj_def.rb | 8 +++++ benchmark/vm_dstr_str.rb | 6 ++++ benchmark/vm_dstr_sym.rb | 6 ++++ common.mk | 2 ++ compile.c | 21 ++++++------- insns.def | 20 ++++++++++-- numeric.c | 33 ++++++++++++++++++-- object.c | 20 ++++++------ test/ruby/test_jit.rb | 8 ++--- test/ruby/test_optimization.rb | 30 ++++++++++++++++++ test/ruby/test_yjit.rb | 8 ++--- vm_insnhelper.c | 64 +++++++++++++++++++++++++++++++++++++++ yjit_codegen.c | 4 +-- 20 files changed, 240 insertions(+), 37 deletions(-) create mode 100644 benchmark/vm_dstr_ary.rb create mode 100644 benchmark/vm_dstr_bool.rb create mode 100644 benchmark/vm_dstr_class_module.rb create mode 100644 benchmark/vm_dstr_digit.rb create mode 100644 benchmark/vm_dstr_int.rb create mode 100644 benchmark/vm_dstr_nil.rb create mode 100644 benchmark/vm_dstr_obj.rb create mode 100644 benchmark/vm_dstr_obj_def.rb create mode 100644 benchmark/vm_dstr_str.rb create mode 100644 benchmark/vm_dstr_sym.rb diff --git a/benchmark/vm_dstr_ary.rb b/benchmark/vm_dstr_ary.rb new file mode 100644 index 00000000000..1d3aa3b97b0 --- /dev/null +++ b/benchmark/vm_dstr_ary.rb @@ -0,0 +1,6 @@ https://github.com/ruby/ruby/blob/trunk/benchmark/vm_dstr_ary.rb#L1 +i = 0 +x = y = [] +while i<6_000_000 # benchmark loop 2 + i += 1 + str = "foo#{x}bar#{y}baz" +end diff --git a/benchmark/vm_dstr_bool.rb b/benchmark/vm_dstr_bool.rb new file mode 100644 index 00000000000..631ca54755c --- /dev/null +++ b/benchmark/vm_dstr_bool.rb @@ -0,0 +1,7 @@ https://github.com/ruby/ruby/blob/trunk/benchmark/vm_dstr_bool.rb#L1 +i = 0 +x = true +y = false +while i<6_000_000 # benchmark loop 2 + i += 1 + str = "foo#{x}bar#{y}baz" +end diff --git a/benchmark/vm_dstr_class_module.rb b/benchmark/vm_dstr_class_module.rb new file mode 100644 index 00000000000..becf0861c7d --- /dev/null +++ b/benchmark/vm_dstr_class_module.rb @@ -0,0 +1,10 @@ https://github.com/ruby/ruby/blob/trunk/benchmark/vm_dstr_class_module.rb#L1 +i = 0 +class A; end unless defined?(A) +module B; end unless defined?(B) +x = A +y = B +while i<6_000_000 # benchmark loop 2 + i += 1 + str = "foo#{x}bar#{y}baz" +end + diff --git a/benchmark/vm_dstr_digit.rb b/benchmark/vm_dstr_digit.rb new file mode 100644 index 00000000000..caaa395192b --- /dev/null +++ b/benchmark/vm_dstr_digit.rb @@ -0,0 +1,7 @@ https://github.com/ruby/ruby/blob/trunk/benchmark/vm_dstr_digit.rb#L1 +i = 0 +x = 0 +y = 9 +while i<6_000_000 # benchmark loop 2 + i += 1 + str = "foo#{x}bar#{y}baz" +end diff --git a/benchmark/vm_dstr_int.rb b/benchmark/vm_dstr_int.rb new file mode 100644 index 00000000000..ed380d75953 --- /dev/null +++ b/benchmark/vm_dstr_int.rb @@ -0,0 +1,5 @@ https://github.com/ruby/ruby/blob/trunk/benchmark/vm_dstr_int.rb#L1 +i = 0 +while i<6_000_000 # benchmark loop 2 + i += 1 + str = "foo#{i}bar#{i}baz" +end diff --git a/benchmark/vm_dstr_nil.rb b/benchmark/vm_dstr_nil.rb new file mode 100644 index 00000000000..ec4f5d6c677 --- /dev/null +++ b/benchmark/vm_dstr_nil.rb @@ -0,0 +1,6 @@ https://github.com/ruby/ruby/blob/trunk/benchmark/vm_dstr_nil.rb#L1 +i = 0 +x = y = nil +while i<6_000_000 # benchmark loop 2 + i += 1 + str = "foo#{x}bar#{y}baz" +end diff --git a/benchmark/vm_dstr_obj.rb b/benchmark/vm_dstr_obj.rb new file mode 100644 index 00000000000..fb78637ead6 --- /dev/null +++ b/benchmark/vm_dstr_obj.rb @@ -0,0 +1,6 @@ https://github.com/ruby/ruby/blob/trunk/benchmark/vm_dstr_obj.rb#L1 +i = 0 +x = y = Object.new +while i<6_000_000 # benchmark loop 2 + i += 1 + str = "foo#{x}bar#{y}baz" +end diff --git a/benchmark/vm_dstr_obj_def.rb b/benchmark/vm_dstr_obj_def.rb new file mode 100644 index 00000000000..99ff7b98fbb --- /dev/null +++ b/benchmark/vm_dstr_obj_def.rb @@ -0,0 +1,8 @@ https://github.com/ruby/ruby/blob/trunk/benchmark/vm_dstr_obj_def.rb#L1 +i = 0 +o = Object.new +def o.to_s; -""; end +x = y = o +while i<6_000_000 # benchmark loop 2 + i += 1 + str = "foo#{x}bar#{y}baz" +end diff --git a/benchmark/vm_dstr_str.rb b/benchmark/vm_dstr_str.rb new file mode 100644 index 00000000000..45fc1078926 --- /dev/null +++ b/benchmark/vm_dstr_str.rb @@ -0,0 +1,6 @@ https://github.com/ruby/ruby/blob/trunk/benchmark/vm_dstr_str.rb#L1 +i = 0 +x = y = "" +while i<6_000_000 # benchmark loop 2 + i += 1 + str = "foo#{x}bar#{y}baz" +end diff --git a/benchmark/vm_dstr_sym.rb b/benchmark/vm_dstr_sym.rb new file mode 100644 index 00000000000..484b8f8150f --- /dev/null +++ b/benchmark/vm_dstr_sym.rb @@ -0,0 +1,6 @@ https://github.com/ruby/ruby/blob/trunk/benchmark/vm_dstr_sym.rb#L1 +i = 0 +x = y = :z +while i<6_000_000 # benchmark loop 2 + i += 1 + str = "foo#{x}bar#{y}baz" +end diff --git a/common.mk b/common.mk index c840ec654de..714882a9b0d 100644 --- a/common.mk +++ b/common.mk @@ -9522,6 +9522,7 @@ numeric.$(OBJEXT): $(top_srcdir)/internal/hash.h https://github.com/ruby/ruby/blob/trunk/common.mk#L9522 numeric.$(OBJEXT): $(top_srcdir)/internal/numeric.h numeric.$(OBJEXT): $(top_srcdir)/internal/object.h numeric.$(OBJEXT): $(top_srcdir)/internal/rational.h +numeric.$(OBJEXT): $(top_srcdir)/internal/string.h numeric.$(OBJEXT): $(top_srcdir)/internal/serial.h numeric.$(OBJEXT): $(top_srcdir)/internal/static_assert.h numeric.$(OBJEXT): $(top_srcdir)/internal/util.h @@ -9598,6 +9599,7 @@ numeric.$(OBJEXT): {$(VPATH)}internal/compiler_is/intel.h https://github.com/ruby/ruby/blob/trunk/common.mk#L9599 numeric.$(OBJEXT): {$(VPATH)}internal/compiler_is/msvc.h numeric.$(OBJEXT): {$(VPATH)}internal/compiler_is/sunpro.h numeric.$(OBJEXT): {$(VPATH)}internal/compiler_since.h +numeric.$(OBJEXT): {$(VPATH)}internal/compilers.h numeric.$(OBJEXT): {$(VPATH)}internal/config.h numeric.$(OBJEXT): {$(VPATH)}internal/constant_p.h numeric.$(OBJEXT): {$(VPATH)}internal/core.h diff --git a/compile.c b/compile.c index 8a459b25589..2b92893d801 100644 --- a/compile.c +++ b/compile.c @@ -3271,13 +3271,13 @@ iseq_peephole_optimize(rb_iseq_t *iseq, LINK_ELEMENT *list, const int do_tailcal https://github.com/ruby/ruby/blob/trunk/compile.c#L3271 } } - if (IS_INSN_ID(iobj, tostring)) { + if (IS_INSN_ID(iobj, anytostring)) { LINK_ELEMENT *next = iobj->link.next; /* - * tostring + * anytostring * concatstrings 1 * => - * tostring + * anytostring */ if (IS_INSN(next) && IS_INSN_ID(next, concatstrings) && OPERAND_AT(next, 0) == INT2FIX(1)) { @@ -7642,17 +7642,14 @@ compile_evstr(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, i https://github.com/ruby/ruby/blob/trunk/compile.c#L7642 CHECK(COMPILE_(ret, "nd_body", node, popped)); if (!popped && !all_string_result_p(node)) { - const int line = nd_line(node); const NODE *line_node = node; const unsigned int flag = VM_CALL_FCALL; - LABEL *isstr = NEW_LABEL(line); - ADD_INSN(ret, line_node, dup); - ADD_INSN1(ret, line_node, checktype, INT2FIX(T_STRING)); - ADD_INSNL(ret, line_node, branchif, isstr); - ADD_INSN(ret, line_node, dup); - ADD_SEND_R(ret, line_node, idTo_s, INT2FIX(0), NULL, INT2FIX(flag), NULL); - ADD_INSN(ret, line_node, tostring); - ADD_LABEL(ret, isstr); + + // Note, this dup could be removed if we are willing to change anytostring. It pops + // two VALUEs off the stack when it could work by replacing the top most VALUE. + ADD_INSN(ret, line_node, dup); + ADD_INSN1(ret, line_node, objtostring, new_callinfo(iseq, idTo_s, 0, flag, NULL, FALSE)); + ADD_INSN(ret, line_node, anytostring); } return COMPILE_OK; } diff --git a/insns.def b/insns.def index 371361512f4..f759c1a5c51 100644 --- a/insns.def +++ b/insns.def @@ -381,9 +381,10 @@ concatstrings https://github.com/ruby/ruby/blob/trunk/insns.def#L381 val = rb_str_concat_literals(num, STACK_ADDR_FROM_TOP(num)); } -/* push the result of to_s. */ +/* Convert the result to string if not already a string. + This is used as a backup if t (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/