mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
01b7d5acc7
This speeds up all instance variable access, even when not in verbose mode. Uninitialized instance variable warnings were rarely helpful, and resulted in slower code if you wanted to avoid warnings when run in verbose mode. Implements [Feature #17055]
1212 lines
33 KiB
Ruby
1212 lines
33 KiB
Ruby
# frozen_string_literal: true
|
|
require 'test/unit'
|
|
require 'tmpdir'
|
|
require_relative '../lib/jit_support'
|
|
|
|
# Test for --jit option
|
|
class TestJIT < Test::Unit::TestCase
|
|
include JITSupport
|
|
|
|
IGNORABLE_PATTERNS = [
|
|
/\AJIT recompile: .+\n\z/,
|
|
/\AJIT inline: .+\n\z/,
|
|
/\ASuccessful MJIT finish\n\z/,
|
|
]
|
|
MAX_CACHE_PATTERNS = [
|
|
/\AJIT compaction \([^)]+\): .+\n\z/,
|
|
/\AToo many JIT code, but skipped unloading units for JIT compaction\n\z/,
|
|
/\ANo units can be unloaded -- .+\n\z/,
|
|
]
|
|
|
|
# trace_* insns are not compiled for now...
|
|
TEST_PENDING_INSNS = RubyVM::INSTRUCTION_NAMES.select { |n| n.start_with?('trace_') }.map(&:to_sym) + [
|
|
# not supported yet
|
|
:defineclass,
|
|
|
|
# to be tested
|
|
:invokebuiltin,
|
|
|
|
# never used
|
|
:opt_invokebuiltin_delegate,
|
|
].each do |insn|
|
|
if !RubyVM::INSTRUCTION_NAMES.include?(insn.to_s)
|
|
warn "instruction #{insn.inspect} is not defined but included in TestJIT::TEST_PENDING_INSNS"
|
|
end
|
|
end
|
|
|
|
def self.untested_insns
|
|
@untested_insns ||= (RubyVM::INSTRUCTION_NAMES.map(&:to_sym) - TEST_PENDING_INSNS)
|
|
end
|
|
|
|
def self.setup
|
|
return if defined?(@setup_hooked)
|
|
@setup_hooked = true
|
|
|
|
# ci.rvm.jp caches its build environment. Clean up temporary files left by SEGV.
|
|
if ENV['RUBY_DEBUG']&.include?('ci')
|
|
Dir.glob("#{ENV.fetch('TMPDIR', '/tmp')}/_ruby_mjit_p*u*.*").each do |file|
|
|
puts "test/ruby/test_jit.rb: removing #{file}"
|
|
File.unlink(file)
|
|
end
|
|
end
|
|
|
|
# ruby -w -Itest/lib test/ruby/test_jit.rb
|
|
if $VERBOSE
|
|
at_exit do
|
|
unless TestJIT.untested_insns.empty?
|
|
warn "you may want to add tests for following insns, when you have a chance: #{TestJIT.untested_insns.join(' ')}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def setup
|
|
unless JITSupport.supported?
|
|
skip 'JIT seems not supported on this platform'
|
|
end
|
|
self.class.setup
|
|
end
|
|
|
|
def test_compile_insn_nop
|
|
assert_compile_once('nil rescue true', result_inspect: 'nil', insns: %i[nop])
|
|
end
|
|
|
|
def test_compile_insn_local
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[setlocal_WC_0 getlocal_WC_0])
|
|
begin;
|
|
foo = 1
|
|
foo
|
|
end;
|
|
|
|
insns = %i[setlocal getlocal setlocal_WC_0 getlocal_WC_0 setlocal_WC_1 getlocal_WC_1]
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", success_count: 3, stdout: '168', insns: insns)
|
|
begin;
|
|
def foo
|
|
a = 0
|
|
[1, 2].each do |i|
|
|
a += i
|
|
[3, 4].each do |j|
|
|
a *= j
|
|
end
|
|
end
|
|
a
|
|
end
|
|
|
|
print foo
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_blockparam
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '3', success_count: 2, insns: %i[getblockparam setblockparam])
|
|
begin;
|
|
def foo(&b)
|
|
a = b
|
|
b = 2
|
|
a.call + 2
|
|
end
|
|
|
|
print foo { 1 }
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_getblockparamproxy
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '4', success_count: 3, insns: %i[getblockparamproxy])
|
|
begin;
|
|
def bar(&b)
|
|
b.call
|
|
end
|
|
|
|
def foo(&b)
|
|
bar(&b) * bar(&b)
|
|
end
|
|
|
|
print foo { 2 }
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_getspecial
|
|
assert_compile_once('$1', result_inspect: 'nil', insns: %i[getspecial])
|
|
end
|
|
|
|
def test_compile_insn_setspecial
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'true', insns: %i[setspecial])
|
|
begin;
|
|
true if nil.nil?..nil.nil?
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_instancevariable
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[getinstancevariable setinstancevariable])
|
|
begin;
|
|
@foo = 1
|
|
@foo
|
|
end;
|
|
|
|
# optimized getinstancevariable call
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '33', success_count: 1, min_calls: 2)
|
|
begin;
|
|
class A
|
|
def initialize
|
|
@a = 1
|
|
@b = 2
|
|
end
|
|
|
|
def three
|
|
@a + @b
|
|
end
|
|
end
|
|
|
|
a = A.new
|
|
print(a.three) # set ic
|
|
print(a.three) # inlined ic
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_classvariable
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '1', success_count: 1, insns: %i[getclassvariable setclassvariable])
|
|
begin;
|
|
class Foo
|
|
def self.foo
|
|
@@foo = 1
|
|
@@foo
|
|
end
|
|
end
|
|
|
|
print Foo.foo
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_constant
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[getconstant setconstant])
|
|
begin;
|
|
FOO = 1
|
|
FOO
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_global
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[getglobal setglobal])
|
|
begin;
|
|
$foo = 1
|
|
$foo
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_putnil
|
|
assert_compile_once('nil', result_inspect: 'nil', insns: %i[putnil])
|
|
end
|
|
|
|
def test_compile_insn_putself
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'hello', success_count: 1, insns: %i[putself])
|
|
begin;
|
|
proc { print "hello" }.call
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_putobject
|
|
assert_compile_once('0', result_inspect: '0', insns: %i[putobject_INT2FIX_0_])
|
|
assert_compile_once('1', result_inspect: '1', insns: %i[putobject_INT2FIX_1_])
|
|
assert_compile_once('2', result_inspect: '2', insns: %i[putobject])
|
|
end
|
|
|
|
def test_compile_insn_definemethod_definesmethod
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'helloworld', success_count: 3, insns: %i[definemethod definesmethod])
|
|
begin;
|
|
print 1.times.map {
|
|
def method_definition
|
|
'hello'
|
|
end
|
|
|
|
def self.smethod_definition
|
|
'world'
|
|
end
|
|
|
|
method_definition + smethod_definition
|
|
}.join
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_putspecialobject
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'a', success_count: 2, insns: %i[putspecialobject])
|
|
begin;
|
|
print 1.times.map {
|
|
def a
|
|
'a'
|
|
end
|
|
|
|
alias :b :a
|
|
|
|
b
|
|
}.join
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_putstring_concatstrings_tostring
|
|
assert_compile_once('"a#{}b" + "c"', result_inspect: '"abc"', insns: %i[putstring concatstrings tostring])
|
|
end
|
|
|
|
def test_compile_insn_toregexp
|
|
assert_compile_once('/#{true}/ =~ "true"', result_inspect: '0', insns: %i[toregexp])
|
|
end
|
|
|
|
def test_compile_insn_newarray
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '[1, 2, 3]', insns: %i[newarray])
|
|
begin;
|
|
a, b, c = 1, 2, 3
|
|
[a, b, c]
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_newarraykwsplat
|
|
assert_compile_once('[**{ x: 1 }]', result_inspect: '[{:x=>1}]', insns: %i[newarraykwsplat])
|
|
end
|
|
|
|
def test_compile_insn_intern_duparray
|
|
assert_compile_once('[:"#{0}"] + [1,2,3]', result_inspect: '[:"0", 1, 2, 3]', insns: %i[intern duparray])
|
|
end
|
|
|
|
def test_compile_insn_expandarray
|
|
assert_compile_once('y = [ true, false, nil ]; x, = y; x', result_inspect: 'true', insns: %i[expandarray])
|
|
end
|
|
|
|
def test_compile_insn_concatarray
|
|
assert_compile_once('["t", "r", *x = "u", "e"].join', result_inspect: '"true"', insns: %i[concatarray])
|
|
end
|
|
|
|
def test_compile_insn_splatarray
|
|
assert_compile_once('[*(1..2)]', result_inspect: '[1, 2]', insns: %i[splatarray])
|
|
end
|
|
|
|
def test_compile_insn_newhash
|
|
assert_compile_once('a = 1; { a: a }', result_inspect: '{:a=>1}', insns: %i[newhash])
|
|
end
|
|
|
|
def test_compile_insn_duphash
|
|
assert_compile_once('{ a: 1 }', result_inspect: '{:a=>1}', insns: %i[duphash])
|
|
end
|
|
|
|
def test_compile_insn_newrange
|
|
assert_compile_once('a = 1; 0..a', result_inspect: '0..1', insns: %i[newrange])
|
|
end
|
|
|
|
def test_compile_insn_pop
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[pop])
|
|
begin;
|
|
a = false
|
|
b = 1
|
|
a || b
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_dup
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '3', insns: %i[dup])
|
|
begin;
|
|
a = 1
|
|
a&.+(2)
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_dupn
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'true', insns: %i[dupn])
|
|
begin;
|
|
klass = Class.new
|
|
klass::X ||= true
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_swap_topn
|
|
assert_compile_once('{}["true"] = true', result_inspect: 'true', insns: %i[swap topn])
|
|
end
|
|
|
|
def test_compile_insn_reverse
|
|
assert_compile_once('q, (w, e), r = 1, [2, 3], 4; [q, w, e, r]', result_inspect: '[1, 2, 3, 4]', insns: %i[reverse])
|
|
end
|
|
|
|
def test_compile_insn_reput
|
|
skip "write test"
|
|
end
|
|
|
|
def test_compile_insn_setn
|
|
assert_compile_once('[nil][0] = 1', result_inspect: '1', insns: %i[setn])
|
|
end
|
|
|
|
def test_compile_insn_adjuststack
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'true', insns: %i[adjuststack])
|
|
begin;
|
|
x = [true]
|
|
x[0] ||= nil
|
|
x[0]
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_defined
|
|
assert_compile_once('defined?(a)', result_inspect: 'nil', insns: %i[defined])
|
|
end
|
|
|
|
def test_compile_insn_checkkeyword
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'true', success_count: 1, insns: %i[checkkeyword])
|
|
begin;
|
|
def test(x: rand)
|
|
x
|
|
end
|
|
print test(x: true)
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_tracecoverage
|
|
skip "write test"
|
|
end
|
|
|
|
def test_compile_insn_defineclass
|
|
skip "support this in mjit_compile (low priority)"
|
|
end
|
|
|
|
def test_compile_insn_send
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '1', success_count: 3, insns: %i[send])
|
|
begin;
|
|
print proc { yield_self { 1 } }.call
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_opt_str_freeze
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"foo"', insns: %i[opt_str_freeze])
|
|
begin;
|
|
'foo'.freeze
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_opt_nil_p
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'false', insns: %i[opt_nil_p])
|
|
begin;
|
|
nil.nil?.nil?
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_opt_str_uminus
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"bar"', insns: %i[opt_str_uminus])
|
|
begin;
|
|
-'bar'
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_opt_newarray_max
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '2', insns: %i[opt_newarray_max])
|
|
begin;
|
|
a = 1
|
|
b = 2
|
|
[a, b].max
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_opt_newarray_min
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[opt_newarray_min])
|
|
begin;
|
|
a = 1
|
|
b = 2
|
|
[a, b].min
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_opt_send_without_block
|
|
assert_compile_once('print', result_inspect: 'nil', insns: %i[opt_send_without_block])
|
|
end
|
|
|
|
def test_compile_insn_invokesuper
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '3', success_count: 4, insns: %i[invokesuper])
|
|
begin;
|
|
mod = Module.new {
|
|
def test
|
|
super + 2
|
|
end
|
|
}
|
|
klass = Class.new {
|
|
prepend mod
|
|
def test
|
|
1
|
|
end
|
|
}
|
|
print klass.new.test
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_invokeblock_leave
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '2', success_count: 2, insns: %i[invokeblock leave])
|
|
begin;
|
|
def foo
|
|
yield
|
|
end
|
|
print foo { 2 }
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_throw
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '4', success_count: 2, insns: %i[throw])
|
|
begin;
|
|
def test
|
|
proc do
|
|
if 1+1 == 1
|
|
return 3
|
|
else
|
|
return 4
|
|
end
|
|
5
|
|
end.call
|
|
end
|
|
print test
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_jump_branchif
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: 'nil', insns: %i[jump branchif])
|
|
begin;
|
|
a = false
|
|
1 + 1 while a
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_branchunless
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: '1', insns: %i[branchunless])
|
|
begin;
|
|
a = true
|
|
if a
|
|
1
|
|
else
|
|
2
|
|
end
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_branchnil
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: '3', insns: %i[branchnil])
|
|
begin;
|
|
a = 2
|
|
a&.+(1)
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_checktype
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: '"42"', insns: %i[checktype])
|
|
begin;
|
|
a = '2'
|
|
"4#{a}"
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_inlinecache
|
|
assert_compile_once('Struct', result_inspect: 'Struct', insns: %i[opt_getinlinecache opt_setinlinecache])
|
|
end
|
|
|
|
def test_compile_insn_once
|
|
assert_compile_once('/#{true}/o =~ "true" && $~.to_a', result_inspect: '["true"]', insns: %i[once])
|
|
end
|
|
|
|
def test_compile_insn_checkmatch_opt_case_dispatch
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"world"', insns: %i[checkmatch opt_case_dispatch])
|
|
begin;
|
|
case 'hello'
|
|
when 'hello'
|
|
'world'
|
|
end
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_opt_calc
|
|
assert_compile_once('4 + 2 - ((2 * 3 / 2) % 2)', result_inspect: '5', insns: %i[opt_plus opt_minus opt_mult opt_div opt_mod])
|
|
assert_compile_once('4.0 + 2.0 - ((2.0 * 3.0 / 2.0) % 2.0)', result_inspect: '5.0', insns: %i[opt_plus opt_minus opt_mult opt_div opt_mod])
|
|
assert_compile_once('4 + 2', result_inspect: '6')
|
|
end
|
|
|
|
def test_compile_insn_opt_cmp
|
|
assert_compile_once('(1 == 1) && (1 != 2)', result_inspect: 'true', insns: %i[opt_eq opt_neq])
|
|
end
|
|
|
|
def test_compile_insn_opt_rel
|
|
assert_compile_once('1 < 2 && 1 <= 1 && 2 > 1 && 1 >= 1', result_inspect: 'true', insns: %i[opt_lt opt_le opt_gt opt_ge])
|
|
end
|
|
|
|
def test_compile_insn_opt_ltlt
|
|
assert_compile_once('[1] << 2', result_inspect: '[1, 2]', insns: %i[opt_ltlt])
|
|
end
|
|
|
|
def test_compile_insn_opt_and
|
|
assert_compile_once('1 & 3', result_inspect: '1', insns: %i[opt_and])
|
|
end
|
|
|
|
def test_compile_insn_opt_or
|
|
assert_compile_once('1 | 3', result_inspect: '3', insns: %i[opt_or])
|
|
end
|
|
|
|
def test_compile_insn_opt_aref
|
|
# optimized call (optimized JIT) -> send call
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '21', success_count: 2, min_calls: 1, insns: %i[opt_aref])
|
|
begin;
|
|
obj = Object.new
|
|
def obj.[](h)
|
|
h
|
|
end
|
|
|
|
block = proc { |h| h[1] }
|
|
print block.call({ 1 => 2 })
|
|
print block.call(obj)
|
|
end;
|
|
|
|
# send call -> optimized call (send JIT) -> optimized call
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '122', success_count: 2, min_calls: 2)
|
|
begin;
|
|
obj = Object.new
|
|
def obj.[](h)
|
|
h
|
|
end
|
|
|
|
block = proc { |h| h[1] }
|
|
print block.call(obj)
|
|
print block.call({ 1 => 2 })
|
|
print block.call({ 1 => 2 })
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_opt_aref_with
|
|
assert_compile_once("{ '1' => 2 }['1']", result_inspect: '2', insns: %i[opt_aref_with])
|
|
end
|
|
|
|
def test_compile_insn_opt_aset
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '5', insns: %i[opt_aset opt_aset_with])
|
|
begin;
|
|
hash = { '1' => 2 }
|
|
(hash['2'] = 2) + (hash[1.to_s] = 3)
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_opt_length_size
|
|
assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '4', insns: %i[opt_length opt_size])
|
|
begin;
|
|
array = [1, 2]
|
|
array.length + array.size
|
|
end;
|
|
end
|
|
|
|
def test_compile_insn_opt_empty_p
|
|
assert_compile_once('[].empty?', result_inspect: 'true', insns: %i[opt_empty_p])
|
|
end
|
|
|
|
def test_compile_insn_opt_succ
|
|
assert_compile_once('1.succ', result_inspect: '2', insns: %i[opt_succ])
|
|
end
|
|
|
|
def test_compile_insn_opt_not
|
|
assert_compile_once('!!true', result_inspect: 'true', insns: %i[opt_not])
|
|
end
|
|
|
|
def test_compile_insn_opt_regexpmatch2
|
|
assert_compile_once("/true/ =~ 'true'", result_inspect: '0', insns: %i[opt_regexpmatch2])
|
|
assert_compile_once("'true' =~ /true/", result_inspect: '0', insns: %i[opt_regexpmatch2])
|
|
end
|
|
|
|
def test_compile_insn_opt_invokebuiltin_delegate_leave
|
|
iseq = eval(EnvUtil.invoke_ruby(['-e', <<~'EOS'], '', true).first)
|
|
p RubyVM::InstructionSequence.of("\x00".method(:unpack)).to_a
|
|
EOS
|
|
insns = collect_insns(iseq)
|
|
mark_tested_insn(:opt_invokebuiltin_delegate_leave, used_insns: insns)
|
|
assert_eval_with_jit('print "\x00".unpack("c")', stdout: '[0]', success_count: 1)
|
|
end
|
|
|
|
def test_jit_output
|
|
out, err = eval_with_jit('5.times { puts "MJIT" }', verbose: 1, min_calls: 5)
|
|
assert_equal("MJIT\n" * 5, out)
|
|
assert_match(/^#{JIT_SUCCESS_PREFIX}: block in <main>@-e:1 -> .+_ruby_mjit_p\d+u\d+\.c$/, err)
|
|
assert_match(/^Successful MJIT finish$/, err)
|
|
end
|
|
|
|
def test_nothing_to_unload_with_jit_wait
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'hello', success_count: 11, max_cache: 10, ignorable_patterns: MAX_CACHE_PATTERNS)
|
|
begin;
|
|
def a1() a2() end
|
|
def a2() a3() end
|
|
def a3() a4() end
|
|
def a4() a5() end
|
|
def a5() a6() end
|
|
def a6() a7() end
|
|
def a7() a8() end
|
|
def a8() a9() end
|
|
def a9() a10() end
|
|
def a10() a11() end
|
|
def a11() print('hello') end
|
|
a1
|
|
end;
|
|
end
|
|
|
|
def test_unload_units_on_fiber
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'hello', success_count: 12, max_cache: 10, ignorable_patterns: MAX_CACHE_PATTERNS)
|
|
begin;
|
|
def a1() a2(false); a2(true) end
|
|
def a2(a) a3(a) end
|
|
def a3(a) a4(a) end
|
|
def a4(a) a5(a) end
|
|
def a5(a) a6(a) end
|
|
def a6(a) a7(a) end
|
|
def a7(a) a8(a) end
|
|
def a8(a) a9(a) end
|
|
def a9(a) a10(a) end
|
|
def a10(a)
|
|
if a
|
|
Fiber.new { a11 }.resume
|
|
end
|
|
end
|
|
def a11() print('hello') end
|
|
a1
|
|
end;
|
|
end
|
|
|
|
def test_unload_units_and_compaction
|
|
Dir.mktmpdir("jit_test_unload_units_") do |dir|
|
|
# MIN_CACHE_SIZE is 10
|
|
out, err = eval_with_jit({"TMPDIR"=>dir}, "#{<<~"begin;"}\n#{<<~'end;'}", verbose: 1, min_calls: 1, max_cache: 10)
|
|
begin;
|
|
i = 0
|
|
while i < 11
|
|
eval(<<-EOS)
|
|
def mjit#{i}
|
|
print #{i}
|
|
end
|
|
mjit#{i}
|
|
EOS
|
|
i += 1
|
|
end
|
|
|
|
if defined?(fork)
|
|
# test the child does not try to delete files which are deleted by parent,
|
|
# and test possible deadlock on fork during MJIT unload and JIT compaction on child
|
|
Process.waitpid(Process.fork {})
|
|
end
|
|
end;
|
|
|
|
debug_info = %Q[stdout:\n"""\n#{out}\n"""\n\nstderr:\n"""\n#{err}"""\n]
|
|
assert_equal('012345678910', out, debug_info)
|
|
compactions, errs = err.lines.partition do |l|
|
|
l.match?(/\AJIT compaction \(\d+\.\dms\): Compacted \d+ methods /)
|
|
end
|
|
10.times do |i|
|
|
assert_match(/\A#{JIT_SUCCESS_PREFIX}: mjit#{i}@\(eval\):/, errs[i], debug_info)
|
|
end
|
|
|
|
assert_equal("No units can be unloaded -- incremented max-cache-size to 11 for --jit-wait\n", errs[10], debug_info)
|
|
assert_match(/\A#{JIT_SUCCESS_PREFIX}: mjit10@\(eval\):/, errs[11], debug_info)
|
|
# On --jit-wait, when the number of JIT-ed code reaches --jit-max-cache,
|
|
# it should trigger compaction.
|
|
unless RUBY_PLATFORM.match?(/mswin|mingw/) # compaction is not supported on Windows yet
|
|
assert_equal(1, compactions.size, debug_info)
|
|
end
|
|
|
|
if RUBY_PLATFORM.match?(/mswin/)
|
|
# "Permission Denied" error is preventing to remove so file on AppVeyor/RubyCI.
|
|
skip 'Removing so file is randomly failing on AppVeyor/RubyCI mswin due to Permission Denied.'
|
|
else
|
|
# verify .c files are deleted on unload_units
|
|
assert_send([Dir, :empty?, dir], debug_info)
|
|
end
|
|
end
|
|
end
|
|
|
|
def test_newarraykwsplat_on_stack
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "[nil, [{:type=>:development}]]\n", success_count: 1, insns: %i[newarraykwsplat])
|
|
begin;
|
|
def arr
|
|
[nil, [:type => :development]]
|
|
end
|
|
p arr
|
|
end;
|
|
end
|
|
|
|
def test_local_stack_on_exception
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '3', success_count: 2)
|
|
begin;
|
|
def b
|
|
raise
|
|
rescue
|
|
2
|
|
end
|
|
|
|
def a
|
|
# Calling #b should be vm_exec, not direct mjit_exec.
|
|
# Otherwise `1` on local variable would be purged.
|
|
1 + b
|
|
end
|
|
|
|
print a
|
|
end;
|
|
end
|
|
|
|
def test_local_stack_with_sp_motion_by_blockargs
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '1', success_count: 2)
|
|
begin;
|
|
def b(base)
|
|
1
|
|
end
|
|
|
|
# This method is simple enough to have false in catch_except_p.
|
|
# So local_stack_p would be true in JIT compiler.
|
|
def a
|
|
m = method(:b)
|
|
|
|
# ci->flag has VM_CALL_ARGS_BLOCKARG and cfp->sp is moved in vm_caller_setup_arg_block.
|
|
# So, for this send insn, JIT-ed code should use cfp->sp instead of local variables for stack.
|
|
Module.module_eval(&m)
|
|
end
|
|
|
|
print a
|
|
end;
|
|
end
|
|
|
|
def test_catching_deep_exception
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '1', success_count: 4)
|
|
begin;
|
|
def catch_true(paths, prefixes) # catch_except_p: TRUE
|
|
prefixes.each do |prefix| # catch_except_p: TRUE
|
|
paths.each do |path| # catch_except_p: FALSE
|
|
return path
|
|
end
|
|
end
|
|
end
|
|
|
|
def wrapper(paths, prefixes)
|
|
catch_true(paths, prefixes)
|
|
end
|
|
|
|
print wrapper(['1'], ['2'])
|
|
end;
|
|
end
|
|
|
|
def test_inlined_c_method
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "aaa", success_count: 2, recompile_count: 1, min_calls: 2)
|
|
begin;
|
|
def test(obj, recursive: nil)
|
|
if recursive
|
|
test(recursive)
|
|
end
|
|
obj.to_s
|
|
end
|
|
|
|
print(test('a')) # set #to_s cc to String#to_s (expecting C method)
|
|
print(test('a')) # JIT with #to_s cc: String#to_s
|
|
# update #to_s cd->cc to Symbol#to_s, then go through the Symbol#to_s cd->cc
|
|
# after checking receiver class using inlined #to_s cc with String#to_s.
|
|
print(test('a', recursive: :foo))
|
|
end;
|
|
end
|
|
|
|
def test_inlined_exivar
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "aaa", success_count: 3, recompile_count: 1, min_calls: 2)
|
|
begin;
|
|
class Foo < Hash
|
|
def initialize
|
|
@a = :a
|
|
end
|
|
|
|
def bar
|
|
@a
|
|
end
|
|
end
|
|
|
|
print(Foo.new.bar)
|
|
print(Foo.new.bar) # compile #initialize, #bar -> recompile #bar
|
|
print(Foo.new.bar) # compile #bar with exivar
|
|
end;
|
|
end
|
|
|
|
def test_inlined_undefined_ivar
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "bbb", success_count: 3, min_calls: 3)
|
|
begin;
|
|
class Foo
|
|
def initialize
|
|
@a = :a
|
|
end
|
|
|
|
def bar
|
|
if @b.nil?
|
|
@b = :b
|
|
end
|
|
end
|
|
end
|
|
|
|
print(Foo.new.bar)
|
|
print(Foo.new.bar)
|
|
print(Foo.new.bar)
|
|
end;
|
|
end
|
|
|
|
def test_inlined_setivar_frozen
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "FrozenError\n", success_count: 2, min_calls: 3)
|
|
begin;
|
|
class A
|
|
def a
|
|
@a = 1
|
|
end
|
|
end
|
|
|
|
a = A.new
|
|
a.a
|
|
a.a
|
|
a.a
|
|
a.freeze
|
|
begin
|
|
a.a
|
|
rescue FrozenError => e
|
|
p e.class
|
|
end
|
|
end;
|
|
end
|
|
|
|
def test_attr_reader
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "4nil\nnil\n6", success_count: 2, min_calls: 2)
|
|
begin;
|
|
class A
|
|
attr_reader :a, :b
|
|
|
|
def initialize
|
|
@a = 2
|
|
end
|
|
|
|
def test
|
|
a
|
|
end
|
|
|
|
def undefined
|
|
b
|
|
end
|
|
end
|
|
|
|
a = A.new
|
|
print(a.test * a.test)
|
|
p(a.undefined)
|
|
p(a.undefined)
|
|
|
|
# redefinition
|
|
def a.test
|
|
3
|
|
end
|
|
|
|
print(2 * a.test)
|
|
end;
|
|
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "true", success_count: 1, min_calls: 2)
|
|
begin;
|
|
class Hoge
|
|
attr_reader :foo
|
|
|
|
def initialize
|
|
@foo = []
|
|
@bar = nil
|
|
end
|
|
end
|
|
|
|
class Fuga < Hoge
|
|
def initialize
|
|
@bar = nil
|
|
@foo = []
|
|
end
|
|
end
|
|
|
|
def test(recv)
|
|
recv.foo.empty?
|
|
end
|
|
|
|
hoge = Hoge.new
|
|
fuga = Fuga.new
|
|
|
|
test(hoge) # VM: cc set index=1
|
|
test(hoge) # JIT: compile with index=1
|
|
test(fuga) # JIT -> VM: cc set index=2
|
|
print test(hoge) # JIT: should use index=1, not index=2 in cc
|
|
end;
|
|
end
|
|
|
|
def test_heap_promotion_of_ivar_in_the_middle_of_jit
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "true\ntrue\n", success_count: 2, min_calls: 2)
|
|
begin;
|
|
class A
|
|
def initialize
|
|
@iv0 = nil
|
|
@iv1 = []
|
|
@iv2 = nil
|
|
end
|
|
|
|
def test(add)
|
|
@iv0.nil?
|
|
@iv2.nil?
|
|
add_ivar if add
|
|
@iv1.empty?
|
|
end
|
|
|
|
def add_ivar
|
|
@iv3 = nil
|
|
end
|
|
end
|
|
|
|
a = A.new
|
|
p a.test(false)
|
|
p a.test(true)
|
|
end;
|
|
end
|
|
|
|
def test_jump_to_precompiled_branch
|
|
assert_eval_with_jit("#{<<~'begin;'}\n#{<<~'end;'}", stdout: ".0", success_count: 1, min_calls: 1)
|
|
begin;
|
|
def test(foo)
|
|
".#{foo unless foo == 1}" if true
|
|
end
|
|
print test(0)
|
|
end;
|
|
end
|
|
|
|
def test_clean_so
|
|
if RUBY_PLATFORM.match?(/mswin/)
|
|
skip 'Removing so file is randomly failing on AppVeyor/RubyCI mswin due to Permission Denied.'
|
|
end
|
|
Dir.mktmpdir("jit_test_clean_so_") do |dir|
|
|
code = "x = 0; 10.times {|i|x+=i}"
|
|
eval_with_jit({"TMPDIR"=>dir}, code)
|
|
assert_send([Dir, :empty?, dir])
|
|
eval_with_jit({"TMPDIR"=>dir}, code, save_temps: true)
|
|
assert_not_send([Dir, :empty?, dir])
|
|
end
|
|
end
|
|
|
|
def test_clean_objects_on_exec
|
|
if /mswin|mingw/ =~ RUBY_PLATFORM
|
|
# TODO: check call stack and close handle of code which is not on stack, and remove objects on best-effort basis
|
|
skip 'Removing so file being used does not work on Windows'
|
|
end
|
|
Dir.mktmpdir("jit_test_clean_objects_on_exec_") do |dir|
|
|
eval_with_jit({"TMPDIR"=>dir}, "#{<<~"begin;"}\n#{<<~"end;"}", min_calls: 1)
|
|
begin;
|
|
def a; end; a
|
|
exec "true"
|
|
end;
|
|
error_message = "Undeleted files:\n #{Dir.glob("#{dir}/*").join("\n ")}\n"
|
|
assert_send([Dir, :empty?, dir], error_message)
|
|
end
|
|
end
|
|
|
|
def test_lambda_longjmp
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '5', success_count: 1)
|
|
begin;
|
|
fib = lambda do |x|
|
|
return x if x == 0 || x == 1
|
|
fib.call(x-1) + fib.call(x-2)
|
|
end
|
|
print fib.call(5)
|
|
end;
|
|
end
|
|
|
|
def test_stack_pointer_with_assignment
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "nil\nnil\n", success_count: 1)
|
|
begin;
|
|
2.times do
|
|
a, _ = nil
|
|
p a
|
|
end
|
|
end;
|
|
end
|
|
|
|
def test_frame_omitted_inlining
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "true\ntrue\ntrue\n", success_count: 1, min_calls: 2)
|
|
begin;
|
|
class Integer
|
|
remove_method :zero?
|
|
def zero?
|
|
self == 0
|
|
end
|
|
end
|
|
|
|
3.times do
|
|
p 0.zero?
|
|
end
|
|
end;
|
|
end
|
|
|
|
def test_block_handler_with_possible_frame_omitted_inlining
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "70.0\n70.0\n70.0\n", success_count: 2, min_calls: 2)
|
|
begin;
|
|
def multiply(a, b)
|
|
a *= b
|
|
end
|
|
|
|
3.times do
|
|
p multiply(7.0, 10.0)
|
|
end
|
|
end;
|
|
end
|
|
|
|
def test_builtin_frame_omitted_inlining
|
|
assert_eval_with_jit('0.zero?; 0.zero?; 3.times { p 0.zero? }', stdout: "true\ntrue\ntrue\n", success_count: 1, min_calls: 2)
|
|
end
|
|
|
|
def test_program_counter_with_regexpmatch
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "aa", success_count: 1)
|
|
begin;
|
|
2.times do
|
|
break if /a/ =~ "ab" && !$~[0]
|
|
print $~[0]
|
|
end
|
|
end;
|
|
end
|
|
|
|
def test_pushed_values_with_opt_aset_with
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "{}{}", success_count: 1)
|
|
begin;
|
|
2.times do
|
|
print(Thread.current["a"] = {})
|
|
end
|
|
end;
|
|
end
|
|
|
|
def test_pushed_values_with_opt_aref_with
|
|
assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "nil\nnil\n", success_count: 1)
|
|
begin;
|
|
2.times do
|
|
p(Thread.current["a"])
|
|
end
|
|
end;
|
|
end
|
|
|
|
def test_caller_locations_without_catch_table
|
|
out, _ = eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", min_calls: 1)
|
|
begin;
|
|
def b # 2
|
|
caller_locations.first # 3
|
|
end # 4
|
|
# 5
|
|
def a # 6
|
|
print # <-- don't leave PC here # 7
|
|
b # 8
|
|
end
|
|
puts a
|
|
puts a
|
|
end;
|
|
lines = out.lines
|
|
assert_equal("-e:8:in `a'\n", lines[0])
|
|
assert_equal("-e:8:in `a'\n", lines[1])
|
|
end
|
|
|
|
def test_fork_with_mjit_worker_thread
|
|
Dir.mktmpdir("jit_test_fork_with_mjit_worker_thread_") do |dir|
|
|
# min_calls: 2 to skip fork block
|
|
out, err = eval_with_jit({ "TMPDIR" => dir }, "#{<<~"begin;"}\n#{<<~"end;"}", min_calls: 2, verbose: 1)
|
|
begin;
|
|
def before_fork; end
|
|
def after_fork; end
|
|
|
|
before_fork; before_fork # the child should not delete this .o file
|
|
pid = Process.fork do # this child should not delete shared .pch file
|
|
sleep 2.0 # to prevent mixing outputs on Solaris
|
|
after_fork; after_fork # this child does not share JIT-ed after_fork with parent
|
|
end
|
|
after_fork; after_fork # this parent does not share JIT-ed after_fork with child
|
|
|
|
Process.waitpid(pid)
|
|
end;
|
|
success_count = err.scan(/^#{JIT_SUCCESS_PREFIX}:/).size
|
|
debug_info = "stdout:\n```\n#{out}\n```\n\nstderr:\n```\n#{err}```\n"
|
|
assert_equal(3, success_count, debug_info)
|
|
|
|
# assert no remove error
|
|
assert_equal("Successful MJIT finish\n" * 2, err.gsub(/^#{JIT_SUCCESS_PREFIX}:[^\n]+\n/, ''), debug_info)
|
|
|
|
# ensure objects are deleted
|
|
assert_send([Dir, :empty?, dir], debug_info)
|
|
end
|
|
end if defined?(fork)
|
|
|
|
private
|
|
|
|
# The shortest way to test one proc
|
|
def assert_compile_once(script, result_inspect:, insns: [], uplevel: 1)
|
|
if script.match?(/\A\n.+\n\z/m)
|
|
script = script.gsub(/^/, ' ')
|
|
else
|
|
script = " #{script} "
|
|
end
|
|
assert_eval_with_jit("p proc {#{script}}.call", stdout: "#{result_inspect}\n", success_count: 1, insns: insns, uplevel: uplevel + 1)
|
|
end
|
|
|
|
# Shorthand for normal test cases
|
|
def assert_eval_with_jit(script, stdout: nil, success_count:, recompile_count: nil, min_calls: 1, max_cache: 1000, insns: [], uplevel: 1, ignorable_patterns: [])
|
|
out, err = eval_with_jit(script, verbose: 1, min_calls: min_calls, max_cache: max_cache)
|
|
success_actual = err.scan(/^#{JIT_SUCCESS_PREFIX}:/).size
|
|
recompile_actual = err.scan(/^#{JIT_RECOMPILE_PREFIX}:/).size
|
|
# Add --jit-verbose=2 logs for cl.exe because compiler's error message is suppressed
|
|
# for cl.exe with --jit-verbose=1. See `start_process` in mjit_worker.c.
|
|
if RUBY_PLATFORM.match?(/mswin/) && success_count != success_actual
|
|
out2, err2 = eval_with_jit(script, verbose: 2, min_calls: min_calls, max_cache: max_cache)
|
|
end
|
|
|
|
# Make sure that the script has insns expected to be tested
|
|
used_insns = method_insns(script)
|
|
insns.each do |insn|
|
|
mark_tested_insn(insn, used_insns: used_insns, uplevel: uplevel + 3)
|
|
end
|
|
|
|
suffix = "script:\n#{code_block(script)}\nstderr:\n#{code_block(err)}#{(
|
|
"\nstdout(verbose=2 retry):\n#{code_block(out2)}\nstderr(verbose=2 retry):\n#{code_block(err2)}" if out2 || err2
|
|
)}"
|
|
assert_equal(
|
|
success_count, success_actual,
|
|
"Expected #{success_count} times of JIT success, but succeeded #{success_actual} times.\n\n#{suffix}",
|
|
)
|
|
if recompile_count
|
|
assert_equal(
|
|
recompile_count, recompile_actual,
|
|
"Expected #{success_count} times of JIT recompile, but recompiled #{success_actual} times.\n\n#{suffix}",
|
|
)
|
|
end
|
|
if stdout
|
|
assert_equal(stdout, out, "Expected stdout #{out.inspect} to match #{stdout.inspect} with script:\n#{code_block(script)}")
|
|
end
|
|
err_lines = err.lines.reject! do |l|
|
|
l.chomp.empty? || l.match?(/\A#{JIT_SUCCESS_PREFIX}/) || (IGNORABLE_PATTERNS + ignorable_patterns).any? { |pat| pat.match?(l) }
|
|
end
|
|
unless err_lines.empty?
|
|
warn err_lines.join(''), uplevel: uplevel
|
|
end
|
|
end
|
|
|
|
def mark_tested_insn(insn, used_insns:, uplevel: 1)
|
|
unless used_insns.include?(insn)
|
|
$stderr.puts
|
|
warn "'#{insn}' insn is not included in the script. Actual insns are: #{used_insns.join(' ')}\n", uplevel: uplevel
|
|
end
|
|
TestJIT.untested_insns.delete(insn)
|
|
end
|
|
|
|
# Collect block's insns or defined method's insns, which are expected to be JIT-ed.
|
|
# Note that this intentionally excludes insns in script's toplevel because they are not JIT-ed.
|
|
def method_insns(script)
|
|
insns = []
|
|
RubyVM::InstructionSequence.compile(script).to_a.last.each do |(insn, *args)|
|
|
case insn
|
|
when :send
|
|
insns += collect_insns(args.last)
|
|
when :definemethod, :definesmethod
|
|
insns += collect_insns(args[1])
|
|
when :defineclass
|
|
insns += collect_insns(args[1])
|
|
end
|
|
end
|
|
insns.uniq
|
|
end
|
|
|
|
# Recursively collect insns in iseq_array
|
|
def collect_insns(iseq_array)
|
|
return [] if iseq_array.nil?
|
|
|
|
insns = iseq_array.last.select { |x| x.is_a?(Array) }.map(&:first)
|
|
iseq_array.last.each do |(insn, *args)|
|
|
case insn
|
|
when :definemethod, :definesmethod, :send
|
|
insns += collect_insns(args.last)
|
|
end
|
|
end
|
|
insns
|
|
end
|
|
end
|