diff options
-rw-r--r-- | inits.c | 2 | ||||
-rw-r--r-- | lib/mjit/stats.rb | 68 | ||||
-rw-r--r-- | lib/mjit/x86_assembler.rb | 20 | ||||
-rw-r--r-- | lib/ruby_vm/mjit/c_pointer.rb | 7 | ||||
-rw-r--r-- | lib/ruby_vm/mjit/compiler.rb | 31 | ||||
-rw-r--r-- | mjit.c | 29 | ||||
-rw-r--r-- | mjit.h | 8 | ||||
-rw-r--r-- | mjit.rb | 2 | ||||
-rw-r--r-- | mjit_c.c | 12 | ||||
-rw-r--r-- | mjit_c.h | 8 | ||||
-rw-r--r-- | mjit_c.rb | 35 | ||||
-rwxr-xr-x | tool/mjit/bindgen.rb | 1 | ||||
-rw-r--r-- | vm_insnhelper.h | 9 |
13 files changed, 211 insertions, 21 deletions
@@ -106,8 +106,8 @@ rb_call_builtin_inits(void) BUILTIN(nilclass); BUILTIN(marshal); #if USE_MJIT - BUILTIN(mjit); BUILTIN(mjit_c); + BUILTIN(mjit); #endif Init_builtin_prelude(); } diff --git a/lib/mjit/stats.rb b/lib/mjit/stats.rb new file mode 100644 index 0000000000..263948bc0e --- /dev/null +++ b/lib/mjit/stats.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true +module RubyVM::MJIT + def self.runtime_stats + stats = {} + + # Insn exits + INSNS.each_value do |insn| + exits = C.mjit_insn_exits[insn.bin] + if exits > 0 + stats[:"exit_#{insn.name}"] = exits + end + end + + # Runtime stats + C.rb_mjit_runtime_counters.members.each do |member| + stats[member] = C.rb_mjit_counters.public_send(member) + end + + # Other stats are calculated here + stats[:side_exit_count] = stats.select { |name, _count| name.start_with?('exit_') }.sum(&:last) + if stats[:vm_insns_count] > 0 + retired_in_mjit = stats[:mjit_insns_count] - stats[:side_exit_count] + stats[:total_insns_count] = retired_in_mjit + stats[:vm_insns_count] + stats[:ratio_in_mjit] = 100.0 * retired_in_mjit / stats[:total_insns_count] + end + + stats + end + + at_exit do + if C.mjit_opts.stats + print_stats + end + end + + class << self + private + + def print_stats + stats = runtime_stats + $stderr.puts("***MJIT: Printing MJIT statistics on exit***") + + $stderr.puts "side_exit_count: #{format('%10d', stats[:side_exit_count])}" + $stderr.puts "total_insns_count: #{format('%10d', stats[:total_insns_count])}" if stats.key?(:total_insns_count) + $stderr.puts "vm_insns_count: #{format('%10d', stats[:vm_insns_count])}" if stats.key?(:vm_insns_count) + $stderr.puts "mjit_insns_count: #{format('%10d', stats[:mjit_insns_count])}" + $stderr.puts "ratio_in_yjit: #{format('%9.1f', stats[:ratio_in_mjit])}%" if stats.key?(:ratio_in_mjit) + + print_exit_counts(stats) + end + + def print_exit_counts(stats, how_many: 20, padding: 2) + exits = stats.filter_map { |name, count| [name.to_s.delete_prefix('exit_'), count] if name.start_with?('exit_') }.to_h + return if exits.empty? + + top_exits = exits.sort_by { |_name, count| -count }.first(how_many).to_h + total_exits = exits.values.sum + $stderr.puts "Top-#{top_exits.size} most frequent exit ops (#{format("%.1f", 100.0 * top_exits.values.sum / total_exits)}% of exits):" + + name_width = top_exits.map { |name, _count| name.length }.max + padding + count_width = top_exits.map { |_name, count| count.to_s.length }.max + padding + top_exits.each do |name, count| + ratio = 100.0 * count / total_exits + $stderr.puts "#{format("%#{name_width}s", name)}: #{format("%#{count_width}d", count)} (#{format('%.1f', ratio)}%)" + end + end + end +end diff --git a/lib/mjit/x86_assembler.rb b/lib/mjit/x86_assembler.rb index ad194185ae..890fa2b80a 100644 --- a/lib/mjit/x86_assembler.rb +++ b/lib/mjit/x86_assembler.rb @@ -34,7 +34,7 @@ module RubyVM::MJIT def add(dst, src) case [dst, src] - # ADD r/m64, imm8 + # ADD r/m64, imm8 (Mod 11) in [Symbol => dst_reg, Integer => src_imm] if r64?(dst_reg) && imm8?(src_imm) # REX.W + 83 /0 ib # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8/16/32 @@ -44,6 +44,16 @@ module RubyVM::MJIT mod_rm: mod_rm(mod: 0b11, rm: reg_code(dst_reg)), imm: imm8(src_imm), ) + # ADD r/m64, imm8 (Mod 00) + in [[Symbol => dst_reg], Integer => src_imm] if r64?(dst_reg) && imm8?(src_imm) + # REX.W + 83 /0 ib + # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0x83, + mod_rm: mod_rm(mod: 0b00, rm: reg_code(dst_reg)), # Mod 00: [reg] + imm: imm8(src_imm), + ) else raise NotImplementedError, "add: not-implemented operands: #{dst.inspect}, #{src.inspect}" end @@ -189,6 +199,14 @@ module RubyVM::MJIT @labels[label] = @bytes.size end + def incr_counter(name) + if C.mjit_opts.stats + comment("increment counter #{name}") + mov(:rax, C.rb_mjit_counters[name].to_i) + add([:rax], 1) # TODO: lock + end + end + private def insn(prefix: nil, opcode:, mod_rm: nil, disp: nil, imm: nil) diff --git a/lib/ruby_vm/mjit/c_pointer.rb b/lib/ruby_vm/mjit/c_pointer.rb index f0f34e949e..0ba9baa7cd 100644 --- a/lib/ruby_vm/mjit/c_pointer.rb +++ b/lib/ruby_vm/mjit/c_pointer.rb @@ -57,11 +57,8 @@ module RubyVM::MJIT # :nodoc: all # Return the offset to a field define_singleton_method(:offsetof) { |field| members.fetch(field).last / 8 } - # Get the offset of a member named +name+ - define_singleton_method(:offsetof) { |name| - _, offset = members.fetch(name) - offset / 8 - } + # Return member names + define_singleton_method(:members) { members.keys } define_method(:initialize) do |addr = nil| if addr.nil? # TODO: get rid of this feature later diff --git a/lib/ruby_vm/mjit/compiler.rb b/lib/ruby_vm/mjit/compiler.rb index 50da0f6fff..3dfea7088e 100644 --- a/lib/ruby_vm/mjit/compiler.rb +++ b/lib/ruby_vm/mjit/compiler.rb @@ -26,6 +26,12 @@ module RubyVM::MJIT # @param ctx [RubyVM::MJIT::Context] # @param asm [RubyVM::MJIT::X86Assembler] def self.compile_exit(jit, ctx, asm) + if C.mjit_opts.stats + insn = decode_insn(C.VALUE.new(jit.pc).*) + asm.comment("increment insn exit: #{insn.name}") + asm.mov(:rax, (C.mjit_insn_exits + insn.bin).to_i) + asm.add([:rax], 1) # TODO: lock + end asm.comment("exit to interpreter") # Update pc @@ -45,6 +51,10 @@ module RubyVM::MJIT asm.ret end + def self.decode_insn(encoded) + INSNS.fetch(C.rb_vm_insn_decode(encoded)) + end + # @param mem_block [Integer] JIT buffer address def initialize(mem_block) @comments = Hash.new { |h, k| h[k] = [] } @@ -59,7 +69,7 @@ module RubyVM::MJIT return if iseq.body.param.flags.has_opt asm = X86Assembler.new - asm.comment("Block: #{iseq.body.location.label}@#{iseq.body.location.pathobj}:#{iseq.body.location.first_lineno}") + asm.comment("Block: #{iseq.body.location.label}@#{pathobj_path(iseq.body.location.pathobj)}:#{iseq.body.location.first_lineno}") compile_prologue(asm) compile_block(asm, iseq) iseq.body.jit_func = compile(asm) @@ -121,7 +131,7 @@ module RubyVM::MJIT index = 0 while index < iseq.body.iseq_size - insn = decode_insn(iseq.body.iseq_encoded[index]) + insn = self.class.decode_insn(iseq.body.iseq_encoded[index]) jit.pc = (iseq.body.iseq_encoded + index).to_i case compile_insn(jit, ctx, asm, insn) @@ -139,7 +149,9 @@ module RubyVM::MJIT # @param ctx [RubyVM::MJIT::Context] # @param asm [RubyVM::MJIT::X86Assembler] def compile_insn(jit, ctx, asm, insn) + asm.incr_counter(:mjit_insns_count) asm.comment("Insn: #{insn.name}") + case insn.name when :putnil then @insn_compiler.putnil(jit, ctx, asm) when :leave then @insn_compiler.leave(jit, ctx, asm) @@ -147,16 +159,12 @@ module RubyVM::MJIT end end - def decode_insn(encoded) - INSNS.fetch(C.rb_vm_insn_decode(encoded)) - end - def dump_disasm(from, to) C.dump_disasm(from, to).each do |address, mnemonic, op_str| @comments.fetch(address, []).each do |comment| puts bold(" # #{comment}") end - puts " 0x#{"%x" % address}: #{mnemonic} #{op_str}" + puts " 0x#{format("%x", address)}: #{mnemonic} #{op_str}" end puts end @@ -164,5 +172,14 @@ module RubyVM::MJIT def bold(text) "\e[1m#{text}\e[0m" end + + # vm_core.h: pathobj_path + def pathobj_path(pathobj) + if pathobj.is_a?(String) + pathobj + else + pathobj.first + end + end end end @@ -300,6 +300,9 @@ mjit_setup_options(const char *s, struct mjit_options *mjit_opt) else if (opt_match_arg(s, l, "call-threshold")) { mjit_opt->call_threshold = atoi(s + 1); } + else if (opt_match_noarg(s, l, "stats")) { + mjit_opt->stats = true; + } // --mjit=pause is an undocumented feature for experiments else if (opt_match_noarg(s, l, "pause")) { mjit_opt->pause = true; @@ -320,10 +323,9 @@ const struct ruby_opt_message mjit_option_messages[] = { M("--mjit-wait", "", "Wait until JIT compilation finishes every time (for testing)"), M("--mjit-save-temps", "", "Save JIT temporary files in $TMP or /tmp (for testing)"), M("--mjit-verbose=num", "", "Print JIT logs of level num or less to stderr (default: 0)"), - M("--mjit-max-cache=num", "", "Max number of methods to be JIT-ed in a cache (default: " - STRINGIZE(DEFAULT_MAX_CACHE_SIZE) ")"), - M("--mjit-call-threshold=num", "", "Number of calls to trigger JIT (for testing, default: " - STRINGIZE(DEFAULT_CALL_THRESHOLD) ")"), + M("--mjit-max-cache=num", "", "Max number of methods to be JIT-ed in a cache (default: " STRINGIZE(DEFAULT_MAX_CACHE_SIZE) ")"), + M("--mjit-call-threshold=num", "", "Number of calls to trigger JIT (for testing, default: " STRINGIZE(DEFAULT_CALL_THRESHOLD) ")"), + M("--mjit-stats", "", "Enable collecting MJIT statistics"), {0} }; #undef M @@ -370,6 +372,22 @@ mjit_compile(FILE *f, const rb_iseq_t *iseq, const char *funcname, int id) // JIT buffer uint8_t *rb_mjit_mem_block = NULL; +#if MJIT_STATS + +struct rb_mjit_runtime_counters rb_mjit_counters = { 0 }; + +// Basically mjit_opts.stats, but this becomes false during MJIT compilation. +static bool mjit_stats_p = false; + +void +rb_mjit_collect_vm_usage_insn(int insn) +{ + if (!mjit_stats_p) return; + rb_mjit_counters.vm_insns_count++; +} + +#endif // YJIT_STATS + void rb_mjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop) { @@ -398,10 +416,12 @@ rb_mjit_compile(const rb_iseq_t *iseq) rb_vm_barrier(); bool original_call_p = mjit_call_p; mjit_call_p = false; // Avoid impacting JIT metrics by itself + mjit_stats_p = false; // Avoid impacting JIT stats by itself VALUE iseq_ptr = rb_funcall(rb_cMJITIseqPtr, rb_intern("new"), 1, SIZET2NUM((size_t)iseq)); rb_funcall(rb_MJITCompiler, rb_intern("call"), 1, iseq_ptr); + mjit_stats_p = mjit_opts.stats; mjit_call_p = original_call_p; RB_VM_LOCK_LEAVE(); } @@ -443,6 +463,7 @@ mjit_init(const struct mjit_options *opts) rb_cMJITIseqPtr = rb_funcall(rb_mMJITC, rb_intern("rb_iseq_t"), 0); mjit_call_p = true; + mjit_stats_p = mjit_opts.stats; // Normalize options if (mjit_opts.call_threshold == 0) @@ -15,6 +15,10 @@ # if USE_MJIT +#ifndef MJIT_STATS +# define MJIT_STATS RUBY_DEBUG +#endif + #include "ruby.h" #include "vm_core.h" @@ -53,6 +57,8 @@ struct mjit_options { bool wait; // Number of calls to trigger JIT compilation. For testing. unsigned int call_threshold; + // Collect MJIT statistics + bool stats; // Force printing info about MJIT work of level VERBOSE or // less. 0=silence, 1=medium, 2=verbose. int verbose; @@ -117,6 +123,7 @@ void mjit_child_after_fork(void); extern void rb_mjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop); extern void rb_mjit_before_ractor_spawn(void); extern void rb_mjit_tracing_invalidate_all(rb_event_flag_t new_iseq_events); +extern void rb_mjit_collect_vm_usage_insn(int insn); # ifdef MJIT_HEADER #define mjit_enabled true @@ -150,6 +157,7 @@ static inline void mjit_finish(bool close_handle_p){} static inline void rb_mjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop) {} static inline void rb_mjit_before_ractor_spawn(void) {} static inline void rb_mjit_tracing_invalidate_all(rb_event_flag_t new_iseq_events) {} +static inline void rb_mjit_collect_vm_usage_insn(int insn) {} # endif // USE_MJIT #endif // RUBY_MJIT_H @@ -23,7 +23,7 @@ if RubyVM::MJIT.enabled? return # miniruby doesn't support MJIT end - RubyVM::MJIT::C = Object.new # forward declaration for mjit/compiler require 'mjit/c_type' require 'mjit/compiler' + require 'mjit/stats' end @@ -38,6 +38,11 @@ #define SIZEOF(type) RB_SIZE2NUM(sizeof(type)) #define SIGNED_TYPE_P(type) RBOOL((type)(-1) < (type)(1)) +#if MJIT_STATS +// Insn side exit counters +static size_t mjit_insn_exits[VM_INSTRUCTION_SIZE] = { 0 }; +#endif // YJIT_STATS + // macOS: brew install capstone // Ubuntu/Debian: apt-get install libcapstone-dev // Fedora: dnf -y install capstone-devel @@ -74,6 +79,13 @@ dump_disasm(rb_execution_context_t *ec, VALUE self, VALUE from, VALUE to) return result; } +// Same as `RubyVM::MJIT.enabled?`, but this is used before it's defined. +static VALUE +mjit_enabled_p(rb_execution_context_t *ec, VALUE self) +{ + return RBOOL(mjit_enabled); +} + #include "mjit_c.rbinc" #endif // USE_MJIT @@ -104,4 +104,12 @@ struct compile_status { extern uint8_t *rb_mjit_mem_block; +#define MJIT_RUNTIME_COUNTERS(...) struct rb_mjit_runtime_counters { size_t __VA_ARGS__; }; +MJIT_RUNTIME_COUNTERS( + vm_insns_count, + mjit_insns_count +) +#undef MJIT_RUNTIME_COUNTERS +extern struct rb_mjit_runtime_counters rb_mjit_counters; + #endif /* MJIT_C_H */ @@ -4,7 +4,7 @@ module RubyVM::MJIT # :nodoc: all # This `class << C` section is for calling C functions. For importing variables # or macros as is, please consider using tool/mjit/bindgen.rb instead. - class << C + class << C = Object.new #======================================================================================== # # New stuff @@ -25,6 +25,28 @@ module RubyVM::MJIT # :nodoc: all } end + def mjit_insn_exits + addr = Primitive.cstmt! %{ + #if MJIT_STATS + return SIZET2NUM((size_t)mjit_insn_exits); + #else + return SIZET2NUM(0); + #endif + } + CType::Immediate.parse("size_t").new(addr) + end + + def rb_mjit_counters + addr = Primitive.cstmt! %{ + #if MJIT_STATS + return SIZET2NUM((size_t)&rb_mjit_counters); + #else + return SIZET2NUM(0); + #endif + } + rb_mjit_runtime_counters.new(addr) + end + # @param from [Integer] - From address # @param to [Integer] - To address def dump_disasm(from, to) @@ -374,6 +396,7 @@ module RubyVM::MJIT # :nodoc: all debug_flags: [CType::Pointer.new { CType::Immediate.parse("char") }, Primitive.cexpr!("OFFSETOF((*((struct mjit_options *)NULL)), debug_flags)")], wait: [self._Bool, Primitive.cexpr!("OFFSETOF((*((struct mjit_options *)NULL)), wait)")], call_threshold: [CType::Immediate.parse("unsigned int"), Primitive.cexpr!("OFFSETOF((*((struct mjit_options *)NULL)), call_threshold)")], + stats: [self._Bool, Primitive.cexpr!("OFFSETOF((*((struct mjit_options *)NULL)), stats)")], verbose: [CType::Immediate.parse("int"), Primitive.cexpr!("OFFSETOF((*((struct mjit_options *)NULL)), verbose)")], max_cache_size: [CType::Immediate.parse("int"), Primitive.cexpr!("OFFSETOF((*((struct mjit_options *)NULL)), max_cache_size)")], pause: [self._Bool, Primitive.cexpr!("OFFSETOF((*((struct mjit_options *)NULL)), pause)")], @@ -657,6 +680,14 @@ module RubyVM::MJIT # :nodoc: all ) end + def C.rb_mjit_runtime_counters + @rb_mjit_runtime_counters ||= CType::Struct.new( + "rb_mjit_runtime_counters", Primitive.cexpr!("SIZEOF(struct rb_mjit_runtime_counters)"), + vm_insns_count: [CType::Immediate.parse("size_t"), Primitive.cexpr!("OFFSETOF((*((struct rb_mjit_runtime_counters *)NULL)), vm_insns_count)")], + mjit_insns_count: [CType::Immediate.parse("size_t"), Primitive.cexpr!("OFFSETOF((*((struct rb_mjit_runtime_counters *)NULL)), mjit_insns_count)")], + ) + end + def C.rb_mjit_unit @rb_mjit_unit ||= CType::Struct.new( "rb_mjit_unit", Primitive.cexpr!("SIZEOF(struct rb_mjit_unit)"), @@ -835,4 +866,4 @@ module RubyVM::MJIT # :nodoc: all end ### MJIT bindgen end ### -end if RubyVM::MJIT.enabled? && RubyVM::MJIT.const_defined?(:C) # not defined for miniruby +end if Primitive.mjit_enabled_p diff --git a/tool/mjit/bindgen.rb b/tool/mjit/bindgen.rb index 1854820ce0..28a17ca003 100755 --- a/tool/mjit/bindgen.rb +++ b/tool/mjit/bindgen.rb @@ -406,6 +406,7 @@ generator = BindingGenerator.new( rb_method_iseq_t rb_method_type_t rb_mjit_compile_info + rb_mjit_runtime_counters rb_mjit_unit rb_serial_t rb_shape diff --git a/vm_insnhelper.h b/vm_insnhelper.h index 51929ba4f2..f787419d6c 100644 --- a/vm_insnhelper.h +++ b/vm_insnhelper.h @@ -20,11 +20,20 @@ RUBY_EXTERN rb_serial_t ruby_vm_global_cvar_state; MJIT_SYMBOL_EXPORT_END +#ifndef MJIT_STATS +# define MJIT_STATS RUBY_DEBUG +#endif + #if VM_COLLECT_USAGE_DETAILS #define COLLECT_USAGE_INSN(insn) vm_collect_usage_insn(insn) #define COLLECT_USAGE_OPERAND(insn, n, op) vm_collect_usage_operand((insn), (n), ((VALUE)(op))) #define COLLECT_USAGE_REGISTER(reg, s) vm_collect_usage_register((reg), (s)) +#elif MJIT_STATS +// for --mjit-stats TODO: make it possible to support both MJIT_STATS and YJIT_STATS +#define COLLECT_USAGE_INSN(insn) rb_mjit_collect_vm_usage_insn(insn) +#define COLLECT_USAGE_OPERAND(insn, n, op) /* none */ +#define COLLECT_USAGE_REGISTER(reg, s) /* none */ #elif YJIT_STATS /* for --yjit-stats */ #define COLLECT_USAGE_INSN(insn) rb_yjit_collect_vm_usage_insn(insn) |