mirror of
https://github.com/ruby/ruby.git
synced 2025-08-26 06:25:31 +02:00

Before this change, when we encounter a constant cache that is specific to a lexical scope, we unconditionally exit. This change falls back to the interpreter's cache in this situation. This should help constant expressions in `class << self`, which is popular at Shopify due to the style guide. This change relies on the cache being warm while compiling to detect the need for checking the lexical scope for simplicity.
254 lines
8.9 KiB
Ruby
254 lines
8.9 KiB
Ruby
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?
|
|
|
|
str = String.new
|
|
str << iseq.disasm
|
|
str << "\n"
|
|
|
|
# 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
|
|
|
|
block_sizes = blocks.map { |block| block.code.length }
|
|
total_bytes = block_sizes.sum
|
|
str << "\n"
|
|
str << "Total code size: #{total_bytes} bytes"
|
|
str << "\n"
|
|
|
|
str
|
|
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)
|
|
cs = YJIT::Disasm.new
|
|
|
|
highlight = ->(comment) { "<b>#{comment}</b>" }
|
|
linebreak = "<br align=\"left\"/>\n"
|
|
|
|
buff = +''
|
|
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"
|
|
buff << " edge [fontname=\"courier\" penwidth=3];\n"
|
|
blocks.each do |block|
|
|
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)
|
|
"b#{block.id} -> b#{id}[label=\"Fall\"];"
|
|
else
|
|
"b#{block.id} -> b#{id}[label=\"Jump\" style=dashed];"
|
|
end
|
|
}.join("\n")
|
|
buff << "\n"
|
|
end
|
|
buff << " }"
|
|
buff << "}"
|
|
buff
|
|
end
|
|
|
|
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
|
|
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_yjit_stats
|
|
end
|
|
|
|
# Discard statistics collected for --yjit-stats.
|
|
def self.reset_stats!
|
|
# defined in yjit_iface.c
|
|
Primitive.reset_stats_bang
|
|
end
|
|
|
|
def self.stats_enabled?
|
|
Primitive.cexpr! 'rb_yjit_opts.gen_stats ? Qtrue : Qfalse'
|
|
end
|
|
|
|
def self.enabled?
|
|
Primitive.cexpr! 'rb_yjit_enabled_p() ? Qtrue : Qfalse'
|
|
end
|
|
|
|
class << self
|
|
private
|
|
|
|
# Format and print out counters
|
|
def _print_stats
|
|
stats = runtime_stats
|
|
return unless stats
|
|
|
|
$stderr.puts("***YJIT: Printing YJIT statistics on exit***")
|
|
|
|
print_counters(stats, prefix: 'send_', prompt: 'method call exit reasons: ')
|
|
print_counters(stats, prefix: 'invokesuper_', prompt: 'invokesuper exit reasons: ')
|
|
print_counters(stats, prefix: 'leave_', prompt: 'leave exit reasons: ')
|
|
print_counters(stats, prefix: 'getivar_', prompt: 'getinstancevariable exit reasons:')
|
|
print_counters(stats, prefix: 'setivar_', prompt: 'setinstancevariable exit reasons:')
|
|
print_counters(stats, prefix: 'oaref_', prompt: 'opt_aref exit reasons: ')
|
|
print_counters(stats, prefix: 'expandarray_', prompt: 'expandarray exit reasons: ')
|
|
print_counters(stats, prefix: 'opt_getinlinecache_', prompt: 'opt_getinlinecache exit reasons: ')
|
|
|
|
side_exits = total_exit_count(stats)
|
|
total_exits = side_exits + stats[:leave_interp_return]
|
|
|
|
# Number of instructions that finish executing in YJIT.
|
|
# See :count-placement: about the subtraction.
|
|
retired_in_yjit = stats[:exec_instruction] - side_exits
|
|
|
|
# Average length of instruction sequences executed by YJIT
|
|
avg_len_in_yjit = retired_in_yjit.to_f / total_exits
|
|
|
|
# Proportion of instructions that retire in YJIT
|
|
total_insns_count = retired_in_yjit + stats[:vm_insns_count]
|
|
yjit_ratio_pct = 100.0 * retired_in_yjit.to_f / total_insns_count
|
|
|
|
$stderr.puts "bindings_allocations: " + ("%10d" % stats[:binding_allocations])
|
|
$stderr.puts "bindings_set: " + ("%10d" % stats[:binding_set])
|
|
$stderr.puts "compiled_iseq_count: " + ("%10d" % stats[:compiled_iseq_count])
|
|
$stderr.puts "compiled_block_count: " + ("%10d" % stats[:compiled_block_count])
|
|
$stderr.puts "invalidation_count: " + ("%10d" % stats[:invalidation_count])
|
|
$stderr.puts "constant_state_bumps: " + ("%10d" % stats[:constant_state_bumps])
|
|
$stderr.puts "inline_code_size: " + ("%10d" % stats[:inline_code_size])
|
|
$stderr.puts "outlined_code_size: " + ("%10d" % stats[:outlined_code_size])
|
|
|
|
$stderr.puts "total_exit_count: " + ("%10d" % total_exits)
|
|
$stderr.puts "total_insns_count: " + ("%10d" % total_insns_count)
|
|
$stderr.puts "vm_insns_count: " + ("%10d" % stats[:vm_insns_count])
|
|
$stderr.puts "yjit_insns_count: " + ("%10d" % stats[:exec_instruction])
|
|
$stderr.puts "ratio_in_yjit: " + ("%9.1f" % yjit_ratio_pct) + "%"
|
|
$stderr.puts "avg_len_in_yjit: " + ("%10.1f" % avg_len_in_yjit)
|
|
|
|
print_sorted_exit_counts(stats, prefix: "exit_")
|
|
end
|
|
|
|
def print_sorted_exit_counts(stats, prefix:, how_many: 20, left_pad: 4)
|
|
exits = []
|
|
stats.each do |k, v|
|
|
if k.start_with?(prefix)
|
|
exits.push [k.to_s.delete_prefix(prefix), v]
|
|
end
|
|
end
|
|
|
|
exits = exits.sort_by { |name, count| -count }[0...how_many]
|
|
total_exits = total_exit_count(stats)
|
|
|
|
top_n_total = exits.map { |name, count| count }.sum
|
|
top_n_exit_pct = 100.0 * top_n_total / total_exits
|
|
|
|
$stderr.puts "Top-#{how_many} most frequent exit ops (#{"%.1f" % top_n_exit_pct}% of exits):"
|
|
|
|
longest_insn_name_len = exits.map { |name, count| name.length }.max
|
|
exits.each do |name, count|
|
|
padding = longest_insn_name_len + left_pad
|
|
padded_name = "%#{padding}s" % name
|
|
padded_count = "%10d" % count
|
|
percent = 100.0 * count / total_exits
|
|
formatted_percent = "%.1f" % percent
|
|
$stderr.puts("#{padded_name}: #{padded_count} (#{formatted_percent}%)" )
|
|
end
|
|
end
|
|
|
|
def total_exit_count(stats, prefix: "exit_")
|
|
total = 0
|
|
stats.each do |k,v|
|
|
total += v if k.start_with?(prefix)
|
|
end
|
|
total
|
|
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
|
|
end
|