1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/test/ruby/test_optimization.rb
Jeremy Evans 1f5f8a187a
Make Array#min/max optimization respect refined methods
Pass in ec to vm_opt_newarray_{max,min}. Avoids having to
call GET_EC inside the functions, for better performance.

While here, add a test for Array#min/max being redefined to
test_optimization.rb.

Fixes [Bug #18180]
2021-09-30 15:18:14 -07:00

906 lines
22 KiB
Ruby

# frozen_string_literal: false
require 'test/unit'
require 'objspace'
class TestRubyOptimization < Test::Unit::TestCase
def assert_redefine_method(klass, method, code, msg = nil)
assert_separately([], "#{<<-"begin;"}\n#{<<~"end;"}")
begin;
class #{klass}
undef #{method}
def #{method}(*args)
args[0]
end
end
#{code}
end;
end
def disasm(name)
RubyVM::InstructionSequence.of(method(name)).disasm
end
def test_fixnum_plus
assert_equal 21, 10 + 11
assert_redefine_method('Integer', '+', 'assert_equal 11, 10 + 11')
end
def test_fixnum_minus
assert_equal 5, 8 - 3
assert_redefine_method('Integer', '-', 'assert_equal 3, 8 - 3')
end
def test_fixnum_mul
assert_equal 15, 3 * 5
assert_redefine_method('Integer', '*', 'assert_equal 5, 3 * 5')
end
def test_fixnum_div
assert_equal 3, 15 / 5
assert_redefine_method('Integer', '/', 'assert_equal 5, 15 / 5')
end
def test_fixnum_mod
assert_equal 1, 8 % 7
assert_redefine_method('Integer', '%', 'assert_equal 7, 8 % 7')
end
def test_fixnum_lt
assert_equal true, 1 < 2
assert_redefine_method('Integer', '<', 'assert_equal 2, 1 < 2')
end
def test_fixnum_le
assert_equal true, 1 <= 2
assert_redefine_method('Integer', '<=', 'assert_equal 2, 1 <= 2')
end
def test_fixnum_gt
assert_equal false, 1 > 2
assert_redefine_method('Integer', '>', 'assert_equal 2, 1 > 2')
end
def test_fixnum_ge
assert_equal false, 1 >= 2
assert_redefine_method('Integer', '>=', 'assert_equal 2, 1 >= 2')
end
def test_float_plus
assert_equal 4.0, 2.0 + 2.0
assert_redefine_method('Float', '+', 'assert_equal 2.0, 2.0 + 2.0')
end
def test_float_minus
assert_equal 4.0, 2.0 + 2.0
assert_redefine_method('Float', '+', 'assert_equal 2.0, 2.0 + 2.0')
end
def test_float_mul
assert_equal 29.25, 4.5 * 6.5
assert_redefine_method('Float', '*', 'assert_equal 6.5, 4.5 * 6.5')
end
def test_float_div
assert_in_delta 0.63063063063063063, 4.2 / 6.66
assert_redefine_method('Float', '/', 'assert_equal 6.66, 4.2 / 6.66', "[Bug #9238]")
end
def test_float_lt
assert_equal true, 1.1 < 2.2
assert_redefine_method('Float', '<', 'assert_equal 2.2, 1.1 < 2.2')
end
def test_float_le
assert_equal true, 1.1 <= 2.2
assert_redefine_method('Float', '<=', 'assert_equal 2.2, 1.1 <= 2.2')
end
def test_float_gt
assert_equal false, 1.1 > 2.2
assert_redefine_method('Float', '>', 'assert_equal 2.2, 1.1 > 2.2')
end
def test_float_ge
assert_equal false, 1.1 >= 2.2
assert_redefine_method('Float', '>=', 'assert_equal 2.2, 1.1 >= 2.2')
end
def test_string_length
assert_equal 6, "string".length
assert_redefine_method('String', 'length', 'assert_nil "string".length')
end
def test_string_size
assert_equal 6, "string".size
assert_redefine_method('String', 'size', 'assert_nil "string".size')
end
def test_string_empty?
assert_equal true, "".empty?
assert_equal false, "string".empty?
assert_redefine_method('String', 'empty?', 'assert_nil "string".empty?')
end
def test_string_plus
assert_equal "", "" + ""
assert_equal "x", "x" + ""
assert_equal "x", "" + "x"
assert_equal "ab", "a" + "b"
assert_redefine_method('String', '+', 'assert_equal "b", "a" + "b"')
end
def test_string_succ
assert_equal 'b', 'a'.succ
assert_equal 'B', 'A'.succ
end
def test_string_format
assert_equal '2', '%d' % 2
assert_redefine_method('String', '%', 'assert_equal 2, "%d" % 2')
end
def test_string_freeze
assert_equal "foo", "foo".freeze
assert_equal "foo".freeze.object_id, "foo".freeze.object_id
assert_redefine_method('String', 'freeze', 'assert_nil "foo".freeze')
end
def test_string_uminus
assert_same "foo".freeze, -"foo"
assert_redefine_method('String', '-@', 'assert_nil(-"foo")')
end
def test_array_min
assert_equal 1, [1, 2, 4].min
assert_redefine_method('Array', 'min', 'assert_nil([1, 2, 4].min)')
assert_redefine_method('Array', 'min', 'assert_nil([1 + 0, 2, 4].min)')
end
def test_array_max
assert_equal 4, [1, 2, 4].max
assert_redefine_method('Array', 'max', 'assert_nil([1, 2, 4].max)')
assert_redefine_method('Array', 'max', 'assert_nil([1 + 0, 2, 4].max)')
end
def test_trace_optimized_methods
bug14870 = "[ruby-core:87638]"
expected = [:-@, :max, :min, :+, :-, :*, :/, :%, :==, :<, :<=, :>, :>=, :<<,
:&, :|, :[], :[]=, :length, :empty?, :nil?, :succ, :!, :=~]
[:c_call, :c_return].each do |type|
methods = []
tp = TracePoint.new(type) { |tp| methods << tp.method_id }
tp.enable do
x = "a"; x = -x
[1].max
[1].min
x = 42 + 2
x = 42 - 2
x = 42 * 2
x = 42 / 2
x = 42 % 2
y = x == 42
y = x < 42
y = x <= 42
y = x > 42
y = x >= 42
x = x << 1
x = x & 1
x = x | 1
x = []; x[1]
x[1] = 2
x.length
x.empty?
x.nil?
x = 1; x.succ
!x
x = 'a'; x =~ /a/
x = y
end
assert_equal(expected, methods, bug14870)
end
methods = []
tp = TracePoint.new(:c_call, :c_return) { |tp| methods << tp.method_id }
tp.enable do
x = 1
x != 42
end
assert_equal([:!=, :==, :==, :!=], methods, bug14870)
end
def test_string_freeze_saves_memory
n = 16384
data = '.'.freeze
r, w = IO.pipe
w.write data
s = r.readpartial(n, '')
assert_operator ObjectSpace.memsize_of(s), :>=, n,
'IO buffer NOT resized prematurely because will likely be reused'
s.freeze
assert_equal ObjectSpace.memsize_of(data), ObjectSpace.memsize_of(s),
'buffer resized on freeze since it cannot be written to again'
ensure
r.close if r
w.close if w
end
def test_string_eq_neq
%w(== !=).each do |m|
assert_redefine_method('String', m, <<-end)
assert_equal :b, ("a" #{m} "b").to_sym
b = 'b'
assert_equal :b, ("a" #{m} b).to_sym
assert_equal :b, (b #{m} "b").to_sym
end
end
end
def test_string_ltlt
assert_equal "", "" << ""
assert_equal "x", "x" << ""
assert_equal "x", "" << "x"
assert_equal "ab", "a" << "b"
assert_redefine_method('String', '<<', 'assert_equal "b", "a" << "b"')
end
def test_fixnum_and
assert_equal 1, 1&3
assert_redefine_method('Integer', '&', 'assert_equal 3, 1&3')
end
def test_fixnum_or
assert_equal 3, 1|3
assert_redefine_method('Integer', '|', 'assert_equal 1, 3|1')
end
def test_array_plus
assert_equal [1,2], [1]+[2]
assert_redefine_method('Array', '+', 'assert_equal [2], [1]+[2]')
end
def test_array_minus
assert_equal [2], [1,2] - [1]
assert_redefine_method('Array', '-', 'assert_equal [1], [1,2]-[1]')
end
def test_array_length
assert_equal 0, [].length
assert_equal 3, [1,2,3].length
assert_redefine_method('Array', 'length', 'assert_nil([].length); assert_nil([1,2,3].length)')
end
def test_array_empty?
assert_equal true, [].empty?
assert_equal false, [1,2,3].empty?
assert_redefine_method('Array', 'empty?', 'assert_nil([].empty?); assert_nil([1,2,3].empty?)')
end
def test_hash_length
assert_equal 0, {}.length
assert_equal 1, {1=>1}.length
assert_redefine_method('Hash', 'length', 'assert_nil({}.length); assert_nil({1=>1}.length)')
end
def test_hash_empty?
assert_equal true, {}.empty?
assert_equal false, {1=>1}.empty?
assert_redefine_method('Hash', 'empty?', 'assert_nil({}.empty?); assert_nil({1=>1}.empty?)')
end
def test_hash_aref_with
h = { "foo" => 1 }
assert_equal 1, h["foo"]
assert_redefine_method('Hash', '[]', "#{<<-"begin;"}\n#{<<~"end;"}")
begin;
h = { "foo" => 1 }
assert_equal "foo", h["foo"]
end;
end
def test_hash_aset_with
h = {}
assert_equal 1, h["foo"] = 1
assert_redefine_method('Hash', '[]=', "#{<<-"begin;"}\n#{<<~"end;"}")
begin;
h = {}
assert_equal 1, h["foo"] = 1, "assignment always returns value set"
assert_nil h["foo"]
end;
end
class MyObj
def ==(other)
true
end
end
def test_eq
assert_equal true, nil == nil
assert_equal true, 1 == 1
assert_equal true, 'string' == 'string'
assert_equal true, 1 == MyObj.new
assert_equal false, nil == MyObj.new
assert_equal true, MyObj.new == 1
assert_equal true, MyObj.new == nil
end
def self.tailcall(klass, src, file = nil, path = nil, line = nil, tailcall: true)
unless file
loc, = caller_locations(1, 1)
file = loc.path
line ||= loc.lineno + 1
end
RubyVM::InstructionSequence.new("proc {|_|_.class_eval {#{src}}}",
file, (path || file), line,
tailcall_optimization: tailcall,
trace_instruction: false)
.eval[klass]
end
def tailcall(*args)
self.class.tailcall(singleton_class, *args)
end
def test_tailcall
bug4082 = '[ruby-core:33289]'
tailcall("#{<<-"begin;"}\n#{<<~"end;"}")
begin;
def fact_helper(n, res)
if n == 1
res
else
fact_helper(n - 1, n * res)
end
end
def fact(n)
fact_helper(n, 1)
end
end;
assert_equal(9131, fact(3000).to_s.size, message(bug4082) {disasm(:fact_helper)})
end
def test_tailcall_with_block
bug6901 = '[ruby-dev:46065]'
tailcall("#{<<-"begin;"}\n#{<<~"end;"}")
begin;
def identity(val)
val
end
def delay
-> {
identity(yield)
}
end
end;
assert_equal(123, delay { 123 }.call, message(bug6901) {disasm(:delay)})
end
def just_yield
yield
end
def test_tailcall_inhibited_by_block
tailcall("#{<<-"begin;"}\n#{<<~"end;"}")
begin;
def yield_result
just_yield {:ok}
end
end;
assert_equal(:ok, yield_result, message {disasm(:yield_result)})
end
def do_raise
raise "should be rescued"
end
def errinfo
$!
end
def test_tailcall_inhibited_by_rescue
bug12082 = '[ruby-core:73871] [Bug #12082]'
EnvUtil.suppress_warning {tailcall("#{<<-"begin;"}\n#{<<~"end;"}")}
begin;
def to_be_rescued
return do_raise
1 + 2
rescue
errinfo
end
end;
result = assert_nothing_raised(RuntimeError, message(bug12082) {disasm(:to_be_rescued)}) {
to_be_rescued
}
assert_instance_of(RuntimeError, result, bug12082)
assert_equal("should be rescued", result.message, bug12082)
end
def test_tailcall_symbol_block_arg
bug12565 = '[ruby-core:46065]'
tailcall("#{<<-"begin;"}\n#{<<~"end;"}")
begin;
def apply_one_and_two(&block)
yield(1, 2)
end
def add_one_and_two
apply_one_and_two(&:+)
end
end;
assert_equal(3, add_one_and_two,
message(bug12565) {disasm(:add_one_and_two)})
end
def test_tailcall_interrupted_by_sigint
bug12576 = 'ruby-core:76327'
script = "#{<<-"begin;"}\n#{<<~'end;'}"
begin;
RubyVM::InstructionSequence.compile_option = {
:tailcall_optimization => true,
:trace_instruction => false
}
eval "#{<<~"begin;"}\n#{<<~'end;1'}"
begin;
def foo
foo
end
puts("start")
STDOUT.flush
foo
end;1
end;
status, _err = EnvUtil.invoke_ruby([], "", true, true, **{}) {
|in_p, out_p, err_p, pid|
in_p.write(script)
in_p.close
out_p.gets
sig = :INT
begin
Process.kill(sig, pid)
Timeout.timeout(1) do
*, stat = Process.wait2(pid)
[stat, err_p.read]
end
rescue Timeout::Error
if sig == :INT
sig = :KILL
retry
else
raise
end
end
}
assert_not_equal("SEGV", Signal.signame(status.termsig || 0), bug12576)
end unless /mswin|mingw/ =~ RUBY_PLATFORM
def test_tailcall_condition_block
bug = '[ruby-core:78015] [Bug #12905]'
src = "#{<<-"begin;"}\n#{<<~"end;"}", __FILE__, nil, __LINE__+1
begin;
def run(current, final)
if current < final
run(current+1, final)
else
nil
end
end
end;
obj = Object.new
self.class.tailcall(obj.singleton_class, *src, tailcall: false)
e = assert_raise(SystemStackError) {
obj.run(1, Float::INFINITY)
}
level = e.backtrace_locations.size
obj = Object.new
self.class.tailcall(obj.singleton_class, *src, tailcall: true)
level *= 2
mesg = message {"#{bug}: #{$!.backtrace_locations.size} / #{level} stack levels"}
assert_nothing_raised(SystemStackError, mesg) {
obj.run(1, level)
}
end
def test_tailcall_not_to_grow_stack
skip 'currently JIT-ed code always creates a new stack frame' if defined?(RubyVM::JIT) && RubyVM::JIT.enabled?
bug16161 = '[ruby-core:94881]'
tailcall("#{<<-"begin;"}\n#{<<~"end;"}")
begin;
def foo(n)
return :ok if n < 1
foo(n - 1)
end
end;
assert_nothing_raised(SystemStackError, bug16161) do
assert_equal(:ok, foo(1_000_000), bug16161)
end
end
class Bug10557
def [](_)
block_given?
end
def []=(_, _)
block_given?
end
end
def test_block_given_aset_aref
bug10557 = '[ruby-core:66595]'
assert_equal(true, Bug10557.new.[](nil){}, bug10557)
assert_equal(true, Bug10557.new.[](0){}, bug10557)
assert_equal(true, Bug10557.new.[](false){}, bug10557)
assert_equal(true, Bug10557.new.[](''){}, bug10557)
assert_equal(true, Bug10557.new.[]=(nil, 1){}, bug10557)
assert_equal(true, Bug10557.new.[]=(0, 1){}, bug10557)
assert_equal(true, Bug10557.new.[]=(false, 1){}, bug10557)
assert_equal(true, Bug10557.new.[]=('', 1){}, bug10557)
end
def test_string_freeze_block
assert_separately([], "#{<<-"begin;"}\n#{<<~"end;"}")
begin;
class String
undef freeze
def freeze
block_given?
end
end
assert_equal(true, "block".freeze {})
assert_equal(false, "block".freeze)
end;
end
def test_opt_case_dispatch
code = "#{<<-"begin;"}\n#{<<~"end;"}"
begin;
case foo
when "foo" then :foo
when true then true
when false then false
when :sym then :sym
when 6 then :fix
when nil then nil
when 0.1 then :float
when 0xffffffffffffffff then :big
else
:nomatch
end
end;
check = {
'foo' => :foo,
true => true,
false => false,
:sym => :sym,
6 => :fix,
nil => nil,
0.1 => :float,
0xffffffffffffffff => :big,
}
iseq = RubyVM::InstructionSequence.compile(code)
assert_match %r{\bopt_case_dispatch\b}, iseq.disasm
check.each do |foo, expect|
assert_equal expect, eval("foo = #{foo.inspect}\n#{code}")
end
assert_equal :nomatch, eval("foo = :blah\n#{code}")
check.each do |foo, _|
klass = foo.class.to_s
assert_separately([], "#{<<~"begin;"}\n#{<<~"end;"}")
begin;
class #{klass}
undef ===
def ===(*args)
false
end
end
foo = #{foo.inspect}
ret = #{code}
assert_equal :nomatch, ret, foo.inspect
end;
end
end
def test_eqq
[ nil, true, false, 0.1, :sym, 'str', 0xffffffffffffffff ].each do |v|
k = v.class.to_s
assert_redefine_method(k, '===', "assert_equal(#{v.inspect} === 0, 0)")
end
end
def test_opt_case_dispatch_inf
inf = 1.0/0.0
result = case inf
when 1 then 1
when 0 then 0
else
inf.to_i rescue nil
end
assert_nil result, '[ruby-dev:49423] [Bug #11804]'
end
def test_nil_safe_conditional_assign
bug11816 = '[ruby-core:74993] [Bug #11816]'
assert_ruby_status([], 'nil&.foo &&= false', bug11816)
end
def test_peephole_string_literal_range
code = "#{<<~"begin;"}\n#{<<~"end;"}"
begin;
case ver
when "2.0.0".."2.3.2" then :foo
when "1.8.0"..."1.8.8" then :bar
end
end;
[ true, false ].each do |opt|
iseq = RubyVM::InstructionSequence.compile(code,
frozen_string_literal: opt)
insn = iseq.disasm
assert_match %r{putobject\s+#{Regexp.quote('"1.8.0"..."1.8.8"')}}, insn
assert_match %r{putobject\s+#{Regexp.quote('"2.0.0".."2.3.2"')}}, insn
assert_no_match(/putstring/, insn)
assert_no_match(/newrange/, insn)
end
end
def test_peephole_dstr
code = "#{<<~'begin;'}\n#{<<~'end;'}"
begin;
exp = -'a'
z = 'a'
[exp, -"#{z}"]
end;
[ false, true ].each do |fsl|
iseq = RubyVM::InstructionSequence.compile(code,
frozen_string_literal: fsl)
assert_same(*iseq.eval,
"[ruby-core:85542] [Bug #14475] fsl: #{fsl}")
end
end
def test_branch_condition_backquote
bug = '[ruby-core:80740] [Bug #13444] redefined backquote should be called'
class << self
def `(s)
@q = s
@r
end
end
@q = nil
@r = nil
assert_equal("bar", ("bar" unless `foo`), bug)
assert_equal("foo", @q, bug)
@q = nil
@r = true
assert_equal("bar", ("bar" if `foo`), bug)
assert_equal("foo", @q, bug)
@q = nil
@r = "z"
assert_equal("bar", ("bar" if `foo#{@r}`))
assert_equal("fooz", @q, bug)
end
def test_branch_condition_def
bug = '[ruby-core:80740] [Bug #13444] method should be defined'
c = Class.new do
raise "bug" unless def t;:ok;end
end
assert_nothing_raised(NoMethodError, bug) do
assert_equal(:ok, c.new.t)
end
end
def test_branch_condition_defs
bug = '[ruby-core:80740] [Bug #13444] singleton method should be defined'
raise "bug" unless def self.t;:ok;end
assert_nothing_raised(NameError, bug) do
assert_equal(:ok, t)
end
end
def test_retry_label_in_unreachable_chunk
bug = '[ruby-core:81272] [Bug #13578]'
assert_valid_syntax("#{<<-"begin;"}\n#{<<-"end;"}", bug)
begin;
def t; if false; case 42; when s {}; end; end; end
end;
end
def bptest_yield &b
yield
end
def bptest_yield_pass &b
bptest_yield(&b)
end
def bptest_bp_value &b
b
end
def bptest_bp_pass_bp_value &b
bptest_bp_value(&b)
end
def bptest_binding &b
binding
end
def bptest_set &b
b = Proc.new{2}
end
def test_block_parameter
assert_equal(1, bptest_yield{1})
assert_equal(1, bptest_yield_pass{1})
assert_equal(1, send(:bptest_yield){1})
assert_equal(Proc, bptest_bp_value{}.class)
assert_equal nil, bptest_bp_value
assert_equal(Proc, bptest_bp_pass_bp_value{}.class)
assert_equal nil, bptest_bp_pass_bp_value
assert_equal Proc, bptest_binding{}.local_variable_get(:b).class
assert_equal 2, bptest_set{1}.call
end
def test_block_parameter_should_not_create_objects
assert_separately [], <<-END
def foo &b
end
h1 = {}; h2 = {}
ObjectSpace.count_objects(h1) # rehearsal
GC.start; GC.disable # to disable GC while foo{}
ObjectSpace.count_objects(h1)
foo{}
ObjectSpace.count_objects(h2)
assert_equal 0, h2[:T_DATA] - h1[:T_DATA] # Proc is T_DATA
END
end
def test_peephole_optimization_without_trace
assert_separately [], <<-END
RubyVM::InstructionSequence.compile_option = {trace_instruction: false}
eval "def foo; 1.times{|(a), &b| nil && a}; end"
END
end
def test_clear_unreachable_keyword_args
assert_separately [], <<-END, timeout: 60
script = <<-EOS
if true
else
foo(k1:1)
end
EOS
GC.stress = true
30.times{
RubyVM::InstructionSequence.compile(script)
}
END
end
def test_callinfo_unreachable_path
assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}")
begin;
iseq = RubyVM::InstructionSequence.compile("if false; foo(bar: :baz); else :ok end")
bin = iseq.to_binary
iseq = RubyVM::InstructionSequence.load_from_binary(bin)
assert_instance_of(RubyVM::InstructionSequence, iseq)
assert_equal(:ok, iseq.eval)
end;
end
def test_side_effect_in_popped_splat
bug = '[ruby-core:84340] [Bug #14201]'
eval("{**(bug = nil; {})};42")
assert_nil(bug)
bug = '[ruby-core:85486] [Bug #14459]'
h = {}
assert_equal(bug, eval('{ok: 42, **h}; bug'))
assert_equal(:ok, eval('{ok: bug = :ok, **h}; bug'))
assert_empty(h)
end
def test_overwritten_blockparam
obj = Object.new
def obj.a(&block)
block = 1
return :ok if block
:ng
end
assert_equal(:ok, obj.a())
end
def test_blockparam_in_rescue
obj = Object.new
def obj.foo(&b)
raise
rescue
b.call
end
result = nil
assert_equal(42, obj.foo {result = 42})
assert_equal(42, result)
end
def test_unconditional_branch_to_leave_block
assert_valid_syntax("#{<<~"begin;"}\n#{<<~'end;'}")
begin;
tap {true || tap {}}
end;
end
def test_jump_elimination_with_optimized_out_block
x = Object.new
def x.bug(obj)
if obj || obj
obj = obj
else
raise "[ruby-core:87830] [Bug #14897]"
end
obj
end
assert_equal(:ok, x.bug(:ok))
end
def test_jump_elimination_with_optimized_out_block_2
x = Object.new
def x.bug
a = "aaa"
ok = :NG
if a == "bbb" || a == "ccc" then
a = a
else
ok = :ok
end
ok
end
assert_equal(:ok, x.bug)
end
def test_peephole_jump_after_newarray
i = 0
%w(1) || 2 while (i += 1) < 100
assert_equal(100, i)
end
def test_optimized_empty_ensure
assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}", timeout: 10)
begin;
assert_raise(RuntimeError) {
begin raise ensure nil if nil end
}
end;
end
def test_optimized_rescue
assert_in_out_err("", "#{<<~"begin;"}\n#{<<~'end;'}", [], /END \(RuntimeError\)/)
begin;
if false
begin
require "some_mad_stuff"
rescue LoadError
puts "no mad stuff loaded"
end
end
raise "END"
end;
end
end