ruby--ruby/yjit.rb

179 lines
5.7 KiB
Ruby
Raw Normal View History

module YJIT
if defined?(Disasm)
def self.disasm(iseq, tty: $stdout && $stdout.tty?)
iseq = RubyVM::InstructionSequence.of(iseq)
blocks = YJIT.blocks_for(iseq)
return if blocks.empty?
Directly link libcapstone for easier development This lets us use libcapstone directly from miniruby so we don't need a Ruby Gem to to dev work. Example usage: ```ruby def foo(x) if x < 1 "wow" else "neat" end end iseq = RubyVM::InstructionSequence.of(method(:foo)) puts UJIT.disasm(iseq) 100.times { foo 1 } puts UJIT.disasm(iseq) ``` Then in the terminal ``` $ ./miniruby test.rb == disasm: #<ISeq:foo@test.rb:1 (1,0)-(7,3)> (catch: FALSE) local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] x@0<Arg> 0000 getlocal_WC_0 x@0 ( 2)[LiCa] 0002 putobject_INT2FIX_1_ 0003 opt_lt <calldata!mid:<, argc:1, ARGS_SIMPLE> 0005 branchunless 10 0007 putstring "wow" ( 3)[Li] 0009 leave ( 7)[Re] 0010 putstring "neat" ( 5)[Li] 0012 leave ( 7)[Re] == ISEQ RANGE: 10 -> 10 ======================================================== 0x0: movabs rax, 0x7fe816e2d1a0 0xa: mov qword ptr [rdi], rax 0xd: mov r8, rax 0x10: mov r9, rax 0x13: mov r11, r12 0x16: jmp qword ptr [rax] == ISEQ RANGE: 0 -> 7 ========================================================== 0x0: mov rax, qword ptr [rdi + 0x20] 0x4: mov rax, qword ptr [rax - 0x18] 0x8: mov qword ptr [rdx], rax 0xb: mov qword ptr [rdx + 8], 3 0x13: movabs rax, 0x7fe817808200 0x1d: test byte ptr [rax + 0x3e6], 1 0x24: jne 0x3ffff7b 0x2a: test byte ptr [rdx], 1 0x2d: je 0x3ffff7b 0x33: test byte ptr [rdx + 8], 1 0x37: je 0x3ffff7b 0x3d: mov rax, qword ptr [rdx] 0x40: cmp rax, qword ptr [rdx + 8] 0x44: movabs rax, 0 0x4e: movabs rcx, 0x14 0x58: cmovl rax, rcx 0x5c: mov qword ptr [rdx], rax 0x5f: test qword ptr [rdx], -9 0x66: jne 0x3ffffd5 ``` Make sure to `brew install pkg-config capstone`
2021-01-22 18:43:26 +00:00
str = String.new
str << iseq.disasm
str << "\n"
Directly link libcapstone for easier development This lets us use libcapstone directly from miniruby so we don't need a Ruby Gem to to dev work. Example usage: ```ruby def foo(x) if x < 1 "wow" else "neat" end end iseq = RubyVM::InstructionSequence.of(method(:foo)) puts UJIT.disasm(iseq) 100.times { foo 1 } puts UJIT.disasm(iseq) ``` Then in the terminal ``` $ ./miniruby test.rb == disasm: #<ISeq:foo@test.rb:1 (1,0)-(7,3)> (catch: FALSE) local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] x@0<Arg> 0000 getlocal_WC_0 x@0 ( 2)[LiCa] 0002 putobject_INT2FIX_1_ 0003 opt_lt <calldata!mid:<, argc:1, ARGS_SIMPLE> 0005 branchunless 10 0007 putstring "wow" ( 3)[Li] 0009 leave ( 7)[Re] 0010 putstring "neat" ( 5)[Li] 0012 leave ( 7)[Re] == ISEQ RANGE: 10 -> 10 ======================================================== 0x0: movabs rax, 0x7fe816e2d1a0 0xa: mov qword ptr [rdi], rax 0xd: mov r8, rax 0x10: mov r9, rax 0x13: mov r11, r12 0x16: jmp qword ptr [rax] == ISEQ RANGE: 0 -> 7 ========================================================== 0x0: mov rax, qword ptr [rdi + 0x20] 0x4: mov rax, qword ptr [rax - 0x18] 0x8: mov qword ptr [rdx], rax 0xb: mov qword ptr [rdx + 8], 3 0x13: movabs rax, 0x7fe817808200 0x1d: test byte ptr [rax + 0x3e6], 1 0x24: jne 0x3ffff7b 0x2a: test byte ptr [rdx], 1 0x2d: je 0x3ffff7b 0x33: test byte ptr [rdx + 8], 1 0x37: je 0x3ffff7b 0x3d: mov rax, qword ptr [rdx] 0x40: cmp rax, qword ptr [rdx + 8] 0x44: movabs rax, 0 0x4e: movabs rcx, 0x14 0x58: cmovl rax, rcx 0x5c: mov qword ptr [rdx], rax 0x5f: test qword ptr [rdx], -9 0x66: jne 0x3ffffd5 ``` Make sure to `brew install pkg-config capstone`
2021-01-22 18:43:26 +00:00
# Sort the blocks by increasing addresses
sorted_blocks = blocks.sort_by(&:address)
highlight = ->(str) {
if tty
"\x1b[1m#{str}\x1b[0m"
else
str
end
}
cs = YJIT::Disasm.new
sorted_blocks.each_with_index do |block, i|
str << "== BLOCK #{i+1}/#{blocks.length}: #{block.code.length} BYTES, ISEQ RANGE [#{block.iseq_start_index},#{block.iseq_end_index}) ".ljust(80, "=")
str << "\n"
comments = comments_for(block.address, block.address + block.code.length)
comment_idx = 0
cs.disasm(block.code, block.address).each do |i|
while (comment = comments[comment_idx]) && comment.address <= i.address
str << " ; #{highlight.call(comment.comment)}\n"
comment_idx += 1
end
str << sprintf(
" %<address>08x: %<instruction>s\t%<details>s\n",
address: i.address,
instruction: i.mnemonic,
details: i.op_str
)
end
end
Directly link libcapstone for easier development This lets us use libcapstone directly from miniruby so we don't need a Ruby Gem to to dev work. Example usage: ```ruby def foo(x) if x < 1 "wow" else "neat" end end iseq = RubyVM::InstructionSequence.of(method(:foo)) puts UJIT.disasm(iseq) 100.times { foo 1 } puts UJIT.disasm(iseq) ``` Then in the terminal ``` $ ./miniruby test.rb == disasm: #<ISeq:foo@test.rb:1 (1,0)-(7,3)> (catch: FALSE) local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] x@0<Arg> 0000 getlocal_WC_0 x@0 ( 2)[LiCa] 0002 putobject_INT2FIX_1_ 0003 opt_lt <calldata!mid:<, argc:1, ARGS_SIMPLE> 0005 branchunless 10 0007 putstring "wow" ( 3)[Li] 0009 leave ( 7)[Re] 0010 putstring "neat" ( 5)[Li] 0012 leave ( 7)[Re] == ISEQ RANGE: 10 -> 10 ======================================================== 0x0: movabs rax, 0x7fe816e2d1a0 0xa: mov qword ptr [rdi], rax 0xd: mov r8, rax 0x10: mov r9, rax 0x13: mov r11, r12 0x16: jmp qword ptr [rax] == ISEQ RANGE: 0 -> 7 ========================================================== 0x0: mov rax, qword ptr [rdi + 0x20] 0x4: mov rax, qword ptr [rax - 0x18] 0x8: mov qword ptr [rdx], rax 0xb: mov qword ptr [rdx + 8], 3 0x13: movabs rax, 0x7fe817808200 0x1d: test byte ptr [rax + 0x3e6], 1 0x24: jne 0x3ffff7b 0x2a: test byte ptr [rdx], 1 0x2d: je 0x3ffff7b 0x33: test byte ptr [rdx + 8], 1 0x37: je 0x3ffff7b 0x3d: mov rax, qword ptr [rdx] 0x40: cmp rax, qword ptr [rdx + 8] 0x44: movabs rax, 0 0x4e: movabs rcx, 0x14 0x58: cmovl rax, rcx 0x5c: mov qword ptr [rdx], rax 0x5f: test qword ptr [rdx], -9 0x66: jne 0x3ffffd5 ``` Make sure to `brew install pkg-config capstone`
2021-01-22 18:43:26 +00:00
block_sizes = blocks.map { |block| block.code.length }
total_bytes = block_sizes.sum
str << "\n"
str << "Total code size: #{total_bytes} bytes"
Directly link libcapstone for easier development This lets us use libcapstone directly from miniruby so we don't need a Ruby Gem to to dev work. Example usage: ```ruby def foo(x) if x < 1 "wow" else "neat" end end iseq = RubyVM::InstructionSequence.of(method(:foo)) puts UJIT.disasm(iseq) 100.times { foo 1 } puts UJIT.disasm(iseq) ``` Then in the terminal ``` $ ./miniruby test.rb == disasm: #<ISeq:foo@test.rb:1 (1,0)-(7,3)> (catch: FALSE) local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] x@0<Arg> 0000 getlocal_WC_0 x@0 ( 2)[LiCa] 0002 putobject_INT2FIX_1_ 0003 opt_lt <calldata!mid:<, argc:1, ARGS_SIMPLE> 0005 branchunless 10 0007 putstring "wow" ( 3)[Li] 0009 leave ( 7)[Re] 0010 putstring "neat" ( 5)[Li] 0012 leave ( 7)[Re] == ISEQ RANGE: 10 -> 10 ======================================================== 0x0: movabs rax, 0x7fe816e2d1a0 0xa: mov qword ptr [rdi], rax 0xd: mov r8, rax 0x10: mov r9, rax 0x13: mov r11, r12 0x16: jmp qword ptr [rax] == ISEQ RANGE: 0 -> 7 ========================================================== 0x0: mov rax, qword ptr [rdi + 0x20] 0x4: mov rax, qword ptr [rax - 0x18] 0x8: mov qword ptr [rdx], rax 0xb: mov qword ptr [rdx + 8], 3 0x13: movabs rax, 0x7fe817808200 0x1d: test byte ptr [rax + 0x3e6], 1 0x24: jne 0x3ffff7b 0x2a: test byte ptr [rdx], 1 0x2d: je 0x3ffff7b 0x33: test byte ptr [rdx + 8], 1 0x37: je 0x3ffff7b 0x3d: mov rax, qword ptr [rdx] 0x40: cmp rax, qword ptr [rdx + 8] 0x44: movabs rax, 0 0x4e: movabs rcx, 0x14 0x58: cmovl rax, rcx 0x5c: mov qword ptr [rdx], rax 0x5f: test qword ptr [rdx], -9 0x66: jne 0x3ffffd5 ``` Make sure to `brew install pkg-config capstone`
2021-01-22 18:43:26 +00:00
str << "\n"
str
Directly link libcapstone for easier development This lets us use libcapstone directly from miniruby so we don't need a Ruby Gem to to dev work. Example usage: ```ruby def foo(x) if x < 1 "wow" else "neat" end end iseq = RubyVM::InstructionSequence.of(method(:foo)) puts UJIT.disasm(iseq) 100.times { foo 1 } puts UJIT.disasm(iseq) ``` Then in the terminal ``` $ ./miniruby test.rb == disasm: #<ISeq:foo@test.rb:1 (1,0)-(7,3)> (catch: FALSE) local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] x@0<Arg> 0000 getlocal_WC_0 x@0 ( 2)[LiCa] 0002 putobject_INT2FIX_1_ 0003 opt_lt <calldata!mid:<, argc:1, ARGS_SIMPLE> 0005 branchunless 10 0007 putstring "wow" ( 3)[Li] 0009 leave ( 7)[Re] 0010 putstring "neat" ( 5)[Li] 0012 leave ( 7)[Re] == ISEQ RANGE: 10 -> 10 ======================================================== 0x0: movabs rax, 0x7fe816e2d1a0 0xa: mov qword ptr [rdi], rax 0xd: mov r8, rax 0x10: mov r9, rax 0x13: mov r11, r12 0x16: jmp qword ptr [rax] == ISEQ RANGE: 0 -> 7 ========================================================== 0x0: mov rax, qword ptr [rdi + 0x20] 0x4: mov rax, qword ptr [rax - 0x18] 0x8: mov qword ptr [rdx], rax 0xb: mov qword ptr [rdx + 8], 3 0x13: movabs rax, 0x7fe817808200 0x1d: test byte ptr [rax + 0x3e6], 1 0x24: jne 0x3ffff7b 0x2a: test byte ptr [rdx], 1 0x2d: je 0x3ffff7b 0x33: test byte ptr [rdx + 8], 1 0x37: je 0x3ffff7b 0x3d: mov rax, qword ptr [rdx] 0x40: cmp rax, qword ptr [rdx + 8] 0x44: movabs rax, 0 0x4e: movabs rcx, 0x14 0x58: cmovl rax, rcx 0x5c: mov qword ptr [rdx], rax 0x5f: test qword ptr [rdx], -9 0x66: jne 0x3ffffd5 ``` Make sure to `brew install pkg-config capstone`
2021-01-22 18:43:26 +00:00
end
def self.comments_for(start_address, end_address)
Primitive.comments_for(start_address, end_address)
end
def self.graphviz_for(iseq)
iseq = RubyVM::InstructionSequence.of(iseq)
2021-06-21 23:48:03 +00:00
cs = YJIT::Disasm.new
highlight = ->(comment) { "<b>#{comment}</b>" }
linebreak = "<br align=\"left\"/>\n"
buff = ''
2021-06-21 23:48:03 +00:00
blocks = blocks_for(iseq).sort_by(&:id)
buff << "digraph g {\n"
# Write the iseq info as a legend
buff << " legend [shape=record fontsize=\"30\" fillcolor=\"lightgrey\" style=\"filled\"];\n"
buff << " legend [label=\"{ Instruction Disassembly For: | {#{iseq.base_label}@#{iseq.absolute_path}:#{iseq.first_lineno}}}\"];\n"
# Subgraph contains disassembly
buff << " subgraph disasm {\n"
buff << " node [shape=record fontname=\"courier\"];\n"
2021-06-22 21:33:46 +00:00
buff << " edge [fontname=\"courier\" penwidth=3];\n"
blocks.each do |block|
2021-06-21 23:48:03 +00:00
disasm = disasm_block(cs, block, highlight)
# convert newlines to breaks that graphviz understands
disasm.gsub!(/\n/, linebreak)
# strip leading whitespace
disasm.gsub!(/^\s+/, '')
buff << "b#{block.id} [label=<#{disasm}>];\n"
buff << block.outgoing_ids.map { |id|
next_block = blocks.bsearch { |nb| id <=> nb.id }
if next_block.address == (block.address + block.code.length)
2021-06-22 21:33:46 +00:00
"b#{block.id} -> b#{id}[label=\"Fall\"];"
2021-06-21 23:48:03 +00:00
else
2021-06-22 21:33:46 +00:00
"b#{block.id} -> b#{id}[label=\"Jump\" style=dashed];"
2021-06-21 23:48:03 +00:00
end
}.join("\n")
buff << "\n"
end
2021-06-21 23:48:03 +00:00
buff << " }"
buff << "}"
buff
end
2021-06-21 23:48:03 +00:00
def self.disasm_block(cs, block, highlight)
comments = comments_for(block.address, block.address + block.code.length)
comment_idx = 0
str = ''
cs.disasm(block.code, block.address).each do |i|
while (comment = comments[comment_idx]) && comment.address <= i.address
2021-06-21 23:48:03 +00:00
str << " ; #{highlight.call(comment.comment)}\n"
comment_idx += 1
end
str << sprintf(
" %<address>08x: %<instruction>s\t%<details>s\n",
address: i.address,
instruction: i.mnemonic,
details: i.op_str
)
end
str
end
end
# Return a hash for statistics generated for the --yjit-stats command line option.
# Return nil when option is not passed or unavailable.
def self.runtime_stats
# defined in yjit_iface.c
Primitive.get_stat_counters
end
# Discard statistics collected for --yjit-stats.
def self.reset_stats!
# defined in yjit_iface.c
Primitive.reset_stats_bang
end
class << self
private
# Format and print out counters
def _print_stats
counters = runtime_stats
return unless counters
$stderr.puts("***YJIT: Printing runtime counters from yjit.rb***")
$stderr.puts("Number of bindings allocated: %d\n" % counters[:binding_allocations])
$stderr.puts("Number of locals modified through binding: %d\n" % counters[:binding_set])
Collect statistics about binding allocations / local variable set This commit collects statistics about how many binding objects are allocated as well as the number of local variables set on bindings. Statistics are output along with other YJIT stats. Here is an example of the output: ``` ***YJIT: Printing runtime counters from yjit.rb*** Number of Bindings Allocated: 195 Number of locals modified through binding: 0 opt_send_without_block exit reasons: ivar_get_method 7515891 (40.4%) se_cc_klass_differ 3081330 (16.6%) iseq_argc_mismatch 1564578 ( 8.4%) se_receiver_not_heap 1557663 ( 8.4%) ic_empty 1407064 ( 7.6%) optimized_method 995823 ( 5.4%) iseq_not_simple 819413 ( 4.4%) alias_method 706972 ( 3.8%) bmethod 685253 ( 3.7%) callsite_not_simple 225983 ( 1.2%) kw_splat 25999 ( 0.1%) ivar_set_method 902 ( 0.0%) cfunc_toomany_args 394 ( 0.0%) refined_method 42 ( 0.0%) cfunc_ruby_array_varg 29 ( 0.0%) invalid_cme 4 ( 0.0%) leave exit reasons: se_finish_frame 4067107 (100.0%) se_interrupt 24 ( 0.0%) getinstancevariable exit reasons: undef 121177 (100.0%) idx_out_of_range 5 ( 0.0%) opt_aref exit reasons: (all relevant counters are zero) compiled_iseq_count: 3944 main_block_code_size: 1.1 MiB side_block_code_size: 0.6 MiB vm_insns_count: 1137268516 yjit_exec_insns_count: 414015644 ratio_in_yjit: 26.7% avg_len_in_yjit: 7.5 total_exit_count: 55491789 most frequent exit op: opt_send_without_block: 18587628 (33.5%) opt_getinlinecache: 11075822 (20.0%) send: 4949300 (8.9%) leave: 4067131 (7.3%) defined: 3975196 (7.2%) setinstancevariable: 3567315 (6.4%) invokesuper: 2982163 (5.4%) getblockparamproxy: 2168852 (3.9%) opt_nil_p: 2104524 (3.8%) opt_aref: 2013858 (3.6%) ``` Running RailsBench allocates 195 binding objects but doesn't set any local variables.
2021-04-07 19:51:50 +00:00
print_counters(counters, prefix: 'send_', prompt: 'method call exit reasons: ')
print_counters(counters, prefix: 'leave_', prompt: 'leave exit reasons: ')
print_counters(counters, prefix: 'getivar_', prompt: 'getinstancevariable exit reasons:')
print_counters(counters, prefix: 'setivar_', prompt: 'setinstancevariable exit reasons:')
2021-03-11 16:25:19 +00:00
print_counters(counters, prefix: 'oaref_', prompt: 'opt_aref exit reasons: ')
end
def print_counters(counters, prefix:, prompt:)
$stderr.puts(prompt)
counters = counters.filter { |key, _| key.start_with?(prefix) }
counters.filter! { |_, value| value != 0 }
counters.transform_keys! { |key| key.to_s.delete_prefix(prefix) }
if counters.empty?
$stderr.puts(" (all relevant counters are zero)")
return
end
counters = counters.to_a
counters.sort_by! { |(_, counter_value)| counter_value }
longest_name_length = counters.max_by { |(name, _)| name.length }.first.length
total = counters.sum { |(_, counter_value)| counter_value }
counters.reverse_each do |(name, value)|
percentage = value.fdiv(total) * 100
$stderr.printf(" %*s %10d (%4.1f%%)\n", longest_name_length, name, value, percentage);
end
end
end
Directly link libcapstone for easier development This lets us use libcapstone directly from miniruby so we don't need a Ruby Gem to to dev work. Example usage: ```ruby def foo(x) if x < 1 "wow" else "neat" end end iseq = RubyVM::InstructionSequence.of(method(:foo)) puts UJIT.disasm(iseq) 100.times { foo 1 } puts UJIT.disasm(iseq) ``` Then in the terminal ``` $ ./miniruby test.rb == disasm: #<ISeq:foo@test.rb:1 (1,0)-(7,3)> (catch: FALSE) local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] x@0<Arg> 0000 getlocal_WC_0 x@0 ( 2)[LiCa] 0002 putobject_INT2FIX_1_ 0003 opt_lt <calldata!mid:<, argc:1, ARGS_SIMPLE> 0005 branchunless 10 0007 putstring "wow" ( 3)[Li] 0009 leave ( 7)[Re] 0010 putstring "neat" ( 5)[Li] 0012 leave ( 7)[Re] == ISEQ RANGE: 10 -> 10 ======================================================== 0x0: movabs rax, 0x7fe816e2d1a0 0xa: mov qword ptr [rdi], rax 0xd: mov r8, rax 0x10: mov r9, rax 0x13: mov r11, r12 0x16: jmp qword ptr [rax] == ISEQ RANGE: 0 -> 7 ========================================================== 0x0: mov rax, qword ptr [rdi + 0x20] 0x4: mov rax, qword ptr [rax - 0x18] 0x8: mov qword ptr [rdx], rax 0xb: mov qword ptr [rdx + 8], 3 0x13: movabs rax, 0x7fe817808200 0x1d: test byte ptr [rax + 0x3e6], 1 0x24: jne 0x3ffff7b 0x2a: test byte ptr [rdx], 1 0x2d: je 0x3ffff7b 0x33: test byte ptr [rdx + 8], 1 0x37: je 0x3ffff7b 0x3d: mov rax, qword ptr [rdx] 0x40: cmp rax, qword ptr [rdx + 8] 0x44: movabs rax, 0 0x4e: movabs rcx, 0x14 0x58: cmovl rax, rcx 0x5c: mov qword ptr [rdx], rax 0x5f: test qword ptr [rdx], -9 0x66: jne 0x3ffffd5 ``` Make sure to `brew install pkg-config capstone`
2021-01-22 18:43:26 +00:00
end