mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 16:36:20 +08:00
Resolves https://github.com/Shopify/ruby/issues/1035 If nothing has been inlined skip the eliminate empty inline optimisation pass. We can only do this if no inlining has occurred on any pass. In subsequent passes it's possible for no inlining to occur, but redundant code to be removed, resulting in empty inline frames. By definition of the loop, if `iteration > 0` then inlining has occurred. This is covered by [test_eliminate_empty_inline_frames_with_dead_snapshot](https://github.com/ruby/ruby/blob/1c391ebaeb16b733d7d9900d3b57082bf4c429a3/zjit/src/hir/opt_tests.rs#L2238). It cuts ~4ms off the compile time: build❯ for i in $(seq 0 5); do WARMUP_ITRS=0 MIN_BENCH_ITRS=2 MIN_BENCH_TIME=0 ./ruby --zjit --zjit-stats ruby-bench/benchmarks/lobsters/benchmark.rb 2>&1 | grep eliminate_empty_inline; done compile_hir_eliminate_empty_inline_frames_time: 8ms compile_hir_eliminate_empty_inline_frames_time: 9ms compile_hir_eliminate_empty_inline_frames_time: 9ms compile_hir_eliminate_empty_inline_frames_time: 9ms compile_hir_eliminate_empty_inline_frames_time: 9ms compile_hir_eliminate_empty_inline_frames_time: 9ms vs master: build❯ for i in $(seq 0 5); do WARMUP_ITRS=0 MIN_BENCH_ITRS=2 MIN_BENCH_TIME=0 ./ruby --zjit --zjit-stats ruby-bench/benchmarks/lobsters/benchmark.rb 2>&1 | grep eliminate_empty_inline; done compile_hir_eliminate_empty_inline_frames_time: 13ms compile_hir_eliminate_empty_inline_frames_time: 14ms compile_hir_eliminate_empty_inline_frames_time: 14ms compile_hir_eliminate_empty_inline_frames_time: 13ms compile_hir_eliminate_empty_inline_frames_time: 13ms compile_hir_eliminate_empty_inline_frames_time: 14ms To measure the change this PR also adds compile_hir_eliminate_empty_inline_frames_time_ns to the statistics displayed.
330 lines
12 KiB
Ruby
330 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
# :markup: markdown
|
|
|
|
# This module allows for introspection of ZJIT, CRuby's just-in-time compiler.
|
|
# Everything in the module is highly implementation specific and the API might
|
|
# be less stable compared to the standard library.
|
|
#
|
|
# This module may not exist if ZJIT does not support the particular platform
|
|
# for which CRuby is built.
|
|
module RubyVM::ZJIT
|
|
# Blocks that are called when ZJIT is enabled
|
|
@jit_hooks = []
|
|
# Avoid calling a Ruby method here to avoid interfering with compilation tests
|
|
if Primitive.rb_zjit_get_stats_file_path_p
|
|
at_exit { print_stats_file }
|
|
end
|
|
if Primitive.rb_zjit_print_stats_p
|
|
at_exit { print_stats }
|
|
end
|
|
end
|
|
|
|
class << RubyVM::ZJIT
|
|
# Check if ZJIT is enabled
|
|
def enabled?
|
|
Primitive.cexpr! 'RBOOL(rb_zjit_enabled_p)'
|
|
end
|
|
|
|
# Enable ZJIT compilation.
|
|
def enable
|
|
return false if enabled?
|
|
|
|
if Primitive.cexpr! 'RBOOL(rb_yjit_enabled_p)'
|
|
warn("Only one JIT can be enabled at the same time.")
|
|
return false
|
|
end
|
|
|
|
Primitive.rb_zjit_enable
|
|
end
|
|
|
|
# Check if `--zjit-trace-exits` is used
|
|
def trace_exit_locations_enabled?
|
|
Primitive.rb_zjit_trace_exit_locations_enabled_p
|
|
end
|
|
|
|
# A directive for the compiler to fail to compile the call to this method.
|
|
# To show this to ZJIT, say `::RubyVM::ZJIT.induce_compile_failure!` verbatim.
|
|
# Other forms are too dynamic to detect during compilation.
|
|
#
|
|
# Actually running this method does nothing, whether ZJIT sees the call or not.
|
|
def induce_compile_failure! = nil
|
|
|
|
# A directive for the compiler to exit out of compiled code at the call site of this method.
|
|
# To show this to ZJIT, say `::RubyVM::ZJIT.induce_side_exit!` verbatim.
|
|
# Other forms are too dynamic to detect during compilation.
|
|
#
|
|
# Actually running this method does nothing, whether ZJIT sees the call or not.
|
|
def induce_side_exit! = nil
|
|
|
|
# A directive for the compiler to emit a breakpoint instruction at the call site of this method.
|
|
# To show this to ZJIT, say `::RubyVM::ZJIT.induce_breakpoint!` verbatim.
|
|
# Other forms are too dynamic to detect during compilation.
|
|
def induce_breakpoint! = nil
|
|
|
|
# Monotonically increasing counter representing the amount of wall time
|
|
# nanoseconds spent compiling.
|
|
def total_compile_time_ns
|
|
Primitive.rb_zjit_total_compile_time_ns_get
|
|
end
|
|
|
|
# The maximum amount of time `ZJIT` is allowed to spend compiling.
|
|
# When `total_compile_time_ns` goes over `max_compile_time_ns`, `YJIT`
|
|
# pause compilation until `max_compile_time_ns` is raised.
|
|
# If set to `0`, there is no limit.
|
|
# This is a best-effort functionality, there are situations in which `ZJIT` may
|
|
# still compile even when paused.
|
|
# This feature is intended to be used to control how fast the JIT warms up,
|
|
# to spread the compilation overhead over a longer period of time.
|
|
def max_compile_time_ns
|
|
Primitive.rb_zjit_max_compile_time_ns_get
|
|
end
|
|
|
|
# Set the maximum amount of time YJIT is allowed to spend compiling.
|
|
def max_compile_time_ns=(max_compile_time_ns)
|
|
Primitive.rb_zjit_max_compile_time_ns_set(max_compile_time_ns)
|
|
end
|
|
|
|
# Check if `--zjit-stats` is used
|
|
def stats_enabled?
|
|
Primitive.rb_zjit_stats_enabled_p
|
|
end
|
|
|
|
# Return ZJIT statistics as a Hash
|
|
def stats(target_key = nil)
|
|
Primitive.rb_zjit_stats(target_key)
|
|
end
|
|
|
|
# Discard statistics collected for `--zjit-stats`.
|
|
def reset_stats!
|
|
Primitive.rb_zjit_reset_stats_bang
|
|
end
|
|
|
|
# Get the summary of ZJIT statistics as a String
|
|
def stats_string
|
|
return unless stats = self.stats
|
|
buf = +"***ZJIT: Printing ZJIT statistics on exit***\n"
|
|
|
|
if stats[:guard_type_count]&.nonzero?
|
|
stats[:guard_type_exit_ratio] = stats[:exit_guard_type_failure].to_f / stats[:guard_type_count] * 100
|
|
end
|
|
if stats[:guard_shape_count]&.nonzero?
|
|
stats[:guard_shape_exit_ratio] = stats[:exit_guard_shape_failure].to_f / stats[:guard_shape_count] * 100
|
|
end
|
|
if stats[:code_region_bytes]&.nonzero?
|
|
stats[:side_exit_size_ratio] = stats[:side_exit_size_bytes].to_f / stats[:code_region_bytes] * 100
|
|
end
|
|
if stats[:compile_time_ns]&.nonzero?
|
|
stats[:compile_side_exit_time_ratio] = stats[:compile_side_exit_time_ns].to_f / stats[:compile_time_ns] * 100
|
|
end
|
|
|
|
# Show counters independent from exit_* or dynamic_send_*
|
|
print_counters_with_prefix(prefix: 'not_inlined_cfuncs_', prompt: 'not inlined C methods', buf:, stats:, limit: 20)
|
|
print_counters_with_prefix(prefix: 'ccall_', prompt: 'calls to C functions from JIT code', buf:, stats:, limit: 20)
|
|
print_counters_with_prefix(prefix: 'iseq_calls_count_', prompt: 'most called JIT functions', buf:, stats:, limit: 20)
|
|
# Don't show not_annotated_cfuncs right now because it mostly duplicates not_inlined_cfuncs
|
|
# print_counters_with_prefix(prefix: 'not_annotated_cfuncs_', prompt: 'not annotated C methods', buf:, stats:, limit: 20)
|
|
|
|
# Show fallback counters, ordered by the typical amount of fallbacks for the prefix at the time
|
|
print_counters_with_prefix(prefix: 'unspecialized_send_def_type_', prompt: 'not optimized method types for send', buf:, stats:, limit: 20)
|
|
print_counters_with_prefix(prefix: 'unspecialized_super_def_type_', prompt: 'not optimized method types for super', buf:, stats:, limit: 20)
|
|
print_counters_with_prefix(prefix: 'uncategorized_fallback_yarv_insn_', prompt: 'instructions with uncategorized fallback reason', buf:, stats:, limit: 20)
|
|
print_counters_with_prefix(prefix: 'send_fallback_', prompt: 'send fallback reasons', buf:, stats:, limit: 20)
|
|
print_counters_with_prefix(prefix: 'send_direct_fallback_context_', prompt: 'SendDirect fallback contexts', buf:, stats:, limit: 10)
|
|
print_counters_with_prefix(prefix: 'setivar_fallback_', prompt: 'setivar fallback reasons', buf:, stats:, limit: 5)
|
|
print_counters_with_prefix(prefix: 'getivar_fallback_', prompt: 'getivar fallback reasons', buf:, stats:, limit: 5)
|
|
print_counters_with_prefix(prefix: 'definedivar_fallback_', prompt: 'definedivar fallback reasons', buf:, stats:, limit: 5)
|
|
print_counters_with_prefix(prefix: 'invokeblock_handler_', prompt: 'invokeblock handler', buf:, stats:, limit: 10)
|
|
print_counters_with_prefix(prefix: 'inline_reject_', prompt: 'HIR-level inlining rejection reasons', buf:, stats:, limit: 10)
|
|
|
|
# Show most popular unsupported call features. Because each call can
|
|
# use multiple complex features, a decrease in this number does not
|
|
# necessarily mean an increase in number of optimized calls.
|
|
print_counters_with_prefix(prefix: 'complex_arg_pass_', prompt: 'popular complex argument-parameter features not optimized', buf:, stats:, limit: 10)
|
|
print_counters_with_prefix(prefix: 'caller_splat_profile_', prompt: 'caller splat length profiles', buf:, stats:, limit: 10)
|
|
|
|
# Show exit counters, ordered by the typical amount of exits for the prefix at the time
|
|
print_counters_with_prefix(prefix: 'compile_error_', prompt: 'compile error reasons', buf:, stats:, limit: 20)
|
|
print_counters_with_prefix(prefix: 'unhandled_yarv_insn_', prompt: 'unhandled YARV insns', buf:, stats:, limit: 20)
|
|
print_counters_with_prefix(prefix: 'unhandled_hir_insn_', prompt: 'unhandled HIR insns', buf:, stats:, limit: 20)
|
|
print_counters_with_prefix(prefix: 'exit_', prompt: 'side exit reasons', buf:, stats:, limit: 20)
|
|
|
|
# Show no-prefix counters, having the most important stat `ratio_in_zjit` at the end
|
|
print_counters([
|
|
:send_count,
|
|
:dynamic_send_count,
|
|
:optimized_send_count,
|
|
:iseq_optimized_send_count,
|
|
:inline_cfunc_optimized_send_count,
|
|
:inline_iseq_optimized_send_count,
|
|
:inline_method_count,
|
|
:empty_inline_frame_count,
|
|
:non_variadic_cfunc_optimized_send_count,
|
|
:variadic_cfunc_optimized_send_count,
|
|
:caller_splat_optimized,
|
|
], buf:, stats:, right_align: true, base: :send_count)
|
|
print_counters([
|
|
:dynamic_setivar_count,
|
|
:dynamic_getivar_count,
|
|
:dynamic_definedivar_count,
|
|
|
|
:compiled_iseq_count,
|
|
:compiled_side_exit_count,
|
|
:failed_iseq_count,
|
|
|
|
:compile_time_ns,
|
|
:compile_side_exit_time_ns,
|
|
:compile_side_exit_time_ratio,
|
|
:compile_hir_time_ns,
|
|
:compile_hir_build_time_ns,
|
|
:compile_hir_strength_reduce_time_ns,
|
|
:compile_hir_inline_methods_time_ns,
|
|
:compile_hir_remove_trivial_block_params_time_ns,
|
|
:compile_hir_optimize_load_store_time_ns,
|
|
:compile_hir_canonicalize_time_ns,
|
|
:compile_hir_fold_constants_time_ns,
|
|
:compile_hir_clean_cfg_time_ns,
|
|
:compile_hir_remove_redundant_patch_points_time_ns,
|
|
:compile_hir_remove_duplicate_check_interrupts_time_ns,
|
|
:compile_hir_eliminate_empty_inline_frames_time_ns,
|
|
:compile_hir_eliminate_dead_code_time_ns,
|
|
:compile_lir_time_ns,
|
|
:profile_time_ns,
|
|
:gc_time_ns,
|
|
:invalidation_time_ns,
|
|
|
|
:vm_write_jit_frame_count,
|
|
:vm_write_sp_count,
|
|
:vm_write_locals_count,
|
|
:vm_write_stack_count,
|
|
:vm_write_to_parent_iseq_local_count,
|
|
|
|
:guard_type_count,
|
|
:guard_type_exit_ratio,
|
|
:guard_shape_count,
|
|
:guard_shape_exit_ratio,
|
|
|
|
:load_field_count,
|
|
:store_field_count,
|
|
|
|
:throw_count,
|
|
|
|
:side_exit_size_bytes,
|
|
:side_exit_size_ratio,
|
|
:jit_frame_heap_bytes,
|
|
:jit_frame_region_bytes,
|
|
:code_region_bytes,
|
|
:zjit_alloc_bytes,
|
|
:total_mem_bytes,
|
|
:total_native_stack_bytes,
|
|
|
|
:side_exit_count,
|
|
:total_insn_count,
|
|
:vm_insn_count,
|
|
:zjit_insn_count,
|
|
:ratio_in_zjit,
|
|
], buf:, stats:)
|
|
|
|
buf
|
|
end
|
|
|
|
# Assert that any future ZJIT compilation will return a function pointer
|
|
def assert_compiles # :nodoc:
|
|
Primitive.rb_zjit_assert_compiles
|
|
end
|
|
|
|
# :stopdoc:
|
|
private
|
|
|
|
# Register a block to be called when ZJIT is enabled
|
|
def add_jit_hook(hook)
|
|
@jit_hooks << hook
|
|
end
|
|
|
|
# Run ZJIT hooks registered by `#with_jit`
|
|
def call_jit_hooks
|
|
@jit_hooks.each(&:call)
|
|
@jit_hooks.clear
|
|
end
|
|
|
|
def print_counters(keys, buf:, stats:, right_align: false, base: nil)
|
|
key_pad = keys.map { |key| key.to_s.sub(/_time_ns\z/, '_time').size }.max + 1
|
|
key_align = '-' unless right_align
|
|
value_pad = keys.filter_map { |key| stats[key] }.map { |value| number_with_delimiter(value).size }.max
|
|
|
|
keys.each do |key|
|
|
# Some stats like vm_insn_count and ratio_in_zjit are not supported on the release build
|
|
next unless stats.key?(key)
|
|
value = stats[key]
|
|
if base && key != base
|
|
total = stats[base]
|
|
if total.nonzero?
|
|
ratio = " (%4.1f%%)" % (100.0 * value / total)
|
|
end
|
|
end
|
|
|
|
case key
|
|
when :ratio_in_zjit
|
|
value = '%0.1f%%' % value
|
|
when :guard_type_exit_ratio, :guard_shape_exit_ratio, :side_exit_size_ratio, :compile_side_exit_time_ratio
|
|
value = '%0.1f%%' % value
|
|
when /_time_ns\z/
|
|
key = key.to_s.sub(/_time_ns\z/, '_time')
|
|
value = "#{number_with_delimiter(value / 10**6)}ms"
|
|
else
|
|
value = number_with_delimiter(value)
|
|
end
|
|
|
|
buf << "%#{key_align}*s %*s%s\n" % [key_pad, "#{key}:", value_pad, value, ratio]
|
|
end
|
|
end
|
|
|
|
def print_counters_with_prefix(buf:, stats:, prefix:, prompt:, limit: nil)
|
|
counters = stats.select { |key, value| key.start_with?(prefix) && value > 0 }
|
|
return if counters.empty?
|
|
|
|
counters.transform_keys! { |key| key.to_s.delete_prefix(prefix) }
|
|
total = counters.values.sum
|
|
|
|
counters = counters.to_a
|
|
counters.sort_by! { |_, value| -value }
|
|
counters = counters.first(limit) if limit
|
|
|
|
key_pad = counters.map { |key, _| key.size }.max
|
|
value_pad = counters.map { |_, value| number_with_delimiter(value).size }.max
|
|
|
|
buf << "Top-#{counters.size} " if limit
|
|
buf << "#{prompt}"
|
|
buf << " (%.1f%% of total #{number_with_delimiter(total)})" % (100.0 * counters.map(&:last).sum / total) if limit
|
|
buf << ":\n"
|
|
counters.each do |key, value|
|
|
buf << " %*s: %*s (%4.1f%%)\n" % [key_pad, key, value_pad, number_with_delimiter(value), (100.0 * value / total)]
|
|
end
|
|
end
|
|
|
|
def number_with_delimiter(number)
|
|
s = number.to_s
|
|
i = s.index('.') || s.size
|
|
s.insert(i -= 3, ',') while i > 3
|
|
s
|
|
end
|
|
|
|
# Print ZJIT stats
|
|
def print_stats
|
|
$stderr.write stats_string
|
|
end
|
|
|
|
# Print ZJIT stats to file
|
|
def print_stats_file
|
|
filename = Primitive.rb_zjit_get_stats_file_path_p
|
|
File.open(filename, "wb") do |file|
|
|
if filename.end_with?(".json")
|
|
require "json"
|
|
file.write(JSON.pretty_generate(stats))
|
|
else
|
|
file.write stats_string
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|