ruby-changes:60598
From: Takashi <ko1@a...>
Date: Tue, 31 Mar 2020 15:17:20 +0900 (JST)
Subject: [ruby-changes:60598] b736ea63bd (master): Optimize exivar access on JIT-ed getivar
https://git.ruby-lang.org/ruby.git/commit/?id=b736ea63bd From b736ea63bd4ce4e2fc81dfa73938b39fa70f659c Mon Sep 17 00:00:00 2001 From: Takashi Kokubun <takashikkbn@g...> Date: Mon, 30 Mar 2020 22:27:01 -0700 Subject: Optimize exivar access on JIT-ed getivar JIT support of dd723771c11. $ benchmark-driver -v --rbenv 'before;before --jit;after --jit' benchmark/mjit_exivar.yml --repeat-count=4 before: ruby 2.8.0dev (2020-03-30T12:32:26Z master e5db3da9d3) [x86_64-linux] before --jit: ruby 2.8.0dev (2020-03-30T12:32:26Z master e5db3da9d3) +JIT [x86_64-linux] after --jit: ruby 2.8.0dev (2020-03-31T05:57:24Z mjit-exivar 128625baec) +JIT [x86_64-linux] Calculating ------------------------------------- before before --jit after --jit mjit_exivar 57.944M 53.579M 54.471M i/s - 200.000M times in 3.451588s 3.732772s 3.671687s Comparison: mjit_exivar before: 57944345.1 i/s after --jit: 54470876.7 i/s - 1.06x slower before --jit: 53579483.4 i/s - 1.08x slower diff --git a/benchmark/mjit_exivar.yml b/benchmark/mjit_exivar.yml new file mode 100644 index 0000000..052ca46 --- /dev/null +++ b/benchmark/mjit_exivar.yml @@ -0,0 +1,32 @@ https://github.com/ruby/ruby/blob/trunk/benchmark/mjit_exivar.yml#L1 +prelude: | + # frozen_string_literal: true + class Bench < Hash + def initialize + @exivar = nil + end + + def exivar + @exivar + end + end + + bench = Bench.new + + if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? + jit_min_calls = 10000 + i = 0 + while i < jit_min_calls + bench.exivar + i += 1 + end + RubyVM::MJIT.pause # compile (1) + # issue recompile + bench.exivar + RubyVM::MJIT.resume + RubyVM::MJIT.pause # compile (2) + end + +benchmark: + mjit_exivar: bench.exivar + +loop_count: 200000000 diff --git a/debug_counter.h b/debug_counter.h index b57b3ef..0634fd3 100644 --- a/debug_counter.h +++ b/debug_counter.h @@ -328,6 +328,7 @@ RB_DEBUG_COUNTER(mjit_frame_JT2VM) https://github.com/ruby/ruby/blob/trunk/debug_counter.h#L328 /* MJIT cancel counters */ RB_DEBUG_COUNTER(mjit_cancel) RB_DEBUG_COUNTER(mjit_cancel_ivar_inline) +RB_DEBUG_COUNTER(mjit_cancel_exivar_inline) RB_DEBUG_COUNTER(mjit_cancel_send_inline) RB_DEBUG_COUNTER(mjit_cancel_opt_insn) /* CALL_SIMPLE_METHOD */ RB_DEBUG_COUNTER(mjit_cancel_invalidate_all) diff --git a/mjit.h b/mjit.h index c504112..462d299 100644 --- a/mjit.h +++ b/mjit.h @@ -62,8 +62,10 @@ struct mjit_options { https://github.com/ruby/ruby/blob/trunk/mjit.h#L62 // State of optimization switches struct rb_mjit_compile_info { - // Disable getinstancevariable/setinstancevariable optimizations based on inline cache + // Disable getinstancevariable/setinstancevariable optimizations based on inline cache (T_OBJECT) bool disable_ivar_cache; + // Disable getinstancevariable/setinstancevariable optimizations based on inline cache (FL_EXIVAR) + bool disable_exivar_cache; // Disable send/opt_send_without_block optimizations based on inline cache bool disable_send_cache; // Disable method inlining diff --git a/mjit_compile.c b/mjit_compile.c index cfaa672..5ac5f4c 100644 --- a/mjit_compile.c +++ b/mjit_compile.c @@ -279,6 +279,12 @@ compile_cancel_handler(FILE *f, const struct rb_iseq_constant_body *body, struct https://github.com/ruby/ruby/blob/trunk/mjit_compile.c#L279 fprintf(f, " rb_mjit_recompile_iseq(original_iseq);\n"); fprintf(f, " goto cancel;\n"); + fprintf(f, "\nexivar_cancel:\n"); + fprintf(f, " RB_DEBUG_COUNTER_INC(mjit_cancel_exivar_inline);\n"); + fprintf(f, " rb_mjit_iseq_compile_info(original_iseq->body)->disable_exivar_cache = true;\n"); + fprintf(f, " rb_mjit_recompile_iseq(original_iseq);\n"); + fprintf(f, " goto cancel;\n"); + fprintf(f, "\ncancel:\n"); fprintf(f, " RB_DEBUG_COUNTER_INC(mjit_cancel);\n"); if (status->local_stack_p) { diff --git a/test/lib/jit_support.rb b/test/lib/jit_support.rb index b6f7a58..b44c94f 100644 --- a/test/lib/jit_support.rb +++ b/test/lib/jit_support.rb @@ -3,6 +3,7 @@ require 'rbconfig' https://github.com/ruby/ruby/blob/trunk/test/lib/jit_support.rb#L3 module JITSupport JIT_TIMEOUT = 600 # 10min for each... JIT_SUCCESS_PREFIX = 'JIT success \(\d+\.\dms\)' + JIT_RECOMPILE_PREFIX = 'JIT recompile' JIT_COMPACTION_PREFIX = 'JIT compaction \(\d+\.\dms\)' UNSUPPORTED_COMPILERS = [ %r[\A.*/bin/intel64/icc\b], diff --git a/test/ruby/test_jit.rb b/test/ruby/test_jit.rb index e06561b..ebf4a22 100644 --- a/test/ruby/test_jit.rb +++ b/test/ruby/test_jit.rb @@ -778,6 +778,25 @@ class TestJIT < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_jit.rb#L778 end; end + def test_inlined_exivar + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "aaa", success_count: 3, recompile_count: 1, min_calls: 2) + begin; + class Foo < Hash + def initialize + @a = :a + end + + def bar + @a + end + end + + print(Foo.new.bar) + print(Foo.new.bar) # compile #initialize, #bar -> recompile #bar + print(Foo.new.bar) # compile #bar with exivar + end; + end + def test_inlined_undefined_ivar assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "bbb", success_count: 3, min_calls: 3) begin; @@ -1065,12 +1084,13 @@ class TestJIT < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_jit.rb#L1084 end # Shorthand for normal test cases - def assert_eval_with_jit(script, stdout: nil, success_count:, min_calls: 1, max_cache: 1000, insns: [], uplevel: 1, ignorable_patterns: []) + def assert_eval_with_jit(script, stdout: nil, success_count:, recompile_count: nil, min_calls: 1, max_cache: 1000, insns: [], uplevel: 1, ignorable_patterns: []) out, err = eval_with_jit(script, verbose: 1, min_calls: min_calls, max_cache: max_cache) - actual = err.scan(/^#{JIT_SUCCESS_PREFIX}:/).size + success_actual = err.scan(/^#{JIT_SUCCESS_PREFIX}:/).size + recompile_actual = err.scan(/^#{JIT_RECOMPILE_PREFIX}:/).size # Add --jit-verbose=2 logs for cl.exe because compiler's error message is suppressed # for cl.exe with --jit-verbose=1. See `start_process` in mjit_worker.c. - if RUBY_PLATFORM.match?(/mswin/) && success_count != actual + if RUBY_PLATFORM.match?(/mswin/) && success_count != success_actual out2, err2 = eval_with_jit(script, verbose: 2, min_calls: min_calls, max_cache: max_cache) end @@ -1080,13 +1100,19 @@ class TestJIT < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_jit.rb#L1100 mark_tested_insn(insn, used_insns: used_insns, uplevel: uplevel + 3) end + suffix = "script:\n#{code_block(script)}\nstderr:\n#{code_block(err)}#{( + "\nstdout(verbose=2 retry):\n#{code_block(out2)}\nstderr(verbose=2 retry):\n#{code_block(err2)}" if out2 || err2 + )}" assert_equal( - success_count, actual, - "Expected #{success_count} times of JIT success, but succeeded #{actual} times.\n\n"\ - "script:\n#{code_block(script)}\nstderr:\n#{code_block(err)}#{( - "\nstdout(verbose=2 retry):\n#{code_block(out2)}\nstderr(verbose=2 retry):\n#{code_block(err2)}" if out2 || err2 - )}", + success_count, success_actual, + "Expected #{success_count} times of JIT success, but succeeded #{success_actual} times.\n\n#{suffix}", ) + if recompile_count + assert_equal( + recompile_count, recompile_actual, + "Expected #{success_count} times of JIT recompile, but recompiled #{success_actual} times.\n\n#{suffix}", + ) + end if stdout assert_equal(stdout, out, "Expected stdout #{out.inspect} to match #{stdout.inspect} with script:\n#{code_block(script)}") end diff --git a/tool/ruby_vm/views/_mjit_compile_ivar.erb b/tool/ruby_vm/views/_mjit_compile_ivar.erb index 57f8d14..85850b4 100644 --- a/tool/ruby_vm/views/_mjit_compile_ivar.erb +++ b/tool/ruby_vm/views/_mjit_compile_ivar.erb @@ -16,17 +16,16 @@ https://github.com/ruby/ruby/blob/trunk/tool/ruby_vm/views/_mjit_compile_ivar.erb#L16 % # compiler: Use copied IVC to avoid race condition IVC ic_copy = &(status->is_entries + ((union iseq_inline_storage_entry *)ic - body->is_entries))->iv_cache; % -% # compiler: Consider cfp->self as T_OBJECT if ic_copy->ic_serial is set if (!status->compile_info->disable_ivar_cache && ic_copy->ic_serial) { % # JIT: optimize away motion of sp and pc. This path does not call rb_warning() and so it's always leaf and not `handles_sp`. % # <%= render 'mjit_compile_pc_and_sp', locals: { insn: insn } -%> % -% # JIT: prepare vm_getivar's arguments and variables +% # JIT: prepare vm_getivar/vm_setivar arguments and variables fprintf(f, "{\n"); fprintf(f, " VALUE obj = GET_SELF();\n"); fprintf(f, " const rb_serial_t ic_serial = (rb_serial_t)%"PRI_SERIALT_PREFIX"u;\n", ic_copy->ic_serial); fprintf(f, " const st_index_t index = %"PRIuSIZE";\n", ic_copy->index); -% # JIT: cache hit path of vm_getivar, or cancel JIT. +% # JIT: cache hit path of vm_getivar/vm_setivar, or cancel JIT (recompile it with exivar) % if insn.name == 'setinstancevariable' fprintf(f, " VALUE val = stack[%d];\n", b->stack_size - 1); fprintf(f, " if (LIKELY(RB_TYPE_P(obj, T_OBJECT) && ic_serial == RCLASS_SERIAL(RBASIC(obj)->klass) && index < ROBJECT_NUMIV(obj) && !RB_OBJ_FROZEN(obj))) {\n"); @@ -50,5 +49,33 @@ https://github.com/ruby/ruby/blob/trunk/tool/ruby_vm/views/_mjit_compile_ivar.erb#L49 fprintf(f, "}\n"); break; } +% if insn.name == 'getinstancevariable' + else if (!status->compile_info->disable_exivar_cache && ic_copy->ic_serial) { +% # JIT: optimize away motion of sp and pc. This path does not call rb_warning() and so it's always leaf and not `handles_sp`. +% # <%= render 'mjit_compi (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/