Optimize instructions when creating an array just to call include? (#12123)

* Add opt_duparray_send insn to skip the allocation on `#include?`

If the method isn't going to modify the array we don't need to copy it.
This avoids the allocation / array copy for things like `[:a, :b].include?(x)`.

This adds a BOP for include? and tracks redefinition for it on Array.

Co-authored-by: Andrew Novoselac <andrew.novoselac@shopify.com>

* YJIT: Implement opt_duparray_send include_p

Co-authored-by: Andrew Novoselac <andrew.novoselac@shopify.com>

* Update opt_newarray_send to support simple forms of include?(arg)

Similar to opt_duparray_send but for non-static arrays.

* YJIT: Implement opt_newarray_send include_p

---------

Co-authored-by: Andrew Novoselac <andrew.novoselac@shopify.com>
This commit is contained in:
Randy Stauner 2024-11-26 12:31:08 -07:00 committed by GitHub
parent c1dcd1d496
commit 1dd40ec18a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
Notes: git 2024-11-26 19:31:33 +00:00
Merged-By: maximecb <maximecb@ruby-lang.org>
14 changed files with 573 additions and 156 deletions

View file

@ -1114,6 +1114,33 @@ class TestArray < Test::Unit::TestCase
assert_not_include(a, [1,2])
end
def test_monkey_patch_include?
assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}", timeout: 30)
begin;
$-w = false
class Array
alias :old_include? :include?
def include? x
return true if x == :always
old_include?(x)
end
end
def test
a, c, always = :a, :c, :always
[
[:a, :b].include?(a),
[:a, :b].include?(c),
[:a, :b].include?(always),
]
end
v = test
class Array
alias :include? :old_include?
end
assert_equal [true, false, true], v
end;
end
def test_intersect?
a = @cls[ 1, 2, 3]
assert_send([a, :intersect?, [3]])

View file

@ -1107,4 +1107,113 @@ class TestRubyOptimization < Test::Unit::TestCase
def o.to_s; 1; end
assert_match %r{\A#<Object:0x[0-9a-f]+>\z}, "#{o}"
end
def test_opt_duparray_send_include_p
[
'x = :b; [:a, :b].include?(x)',
'@c = :b; [:a, :b].include?(@c)',
'@c = "b"; %i[a b].include?(@c.to_sym)',
'[:a, :b].include?(self) == false',
].each do |code|
iseq = RubyVM::InstructionSequence.compile(code)
insn = iseq.disasm
assert_match(/opt_duparray_send/, insn)
assert_no_match(/\bduparray\b/, insn)
assert_equal(true, eval(code))
end
x, y = :b, :c
assert_equal(true, [:a, :b].include?(x))
assert_equal(false, [:a, :b].include?(y))
assert_in_out_err([], <<~RUBY, ["1,2", "3,3", "1,2", "4,4"])
class Array
prepend(Module.new do
def include?(i)
puts self.join(",")
# Modify self to prove that we are operating on a copy.
map! { i }
puts self.join(",")
end
end)
end
def x(i)
[1, 2].include?(i)
end
x(3)
x(4)
RUBY
# Ensure raises happen correctly.
assert_in_out_err([], <<~RUBY, ["will raise", "int 1 not 3"])
class Integer
undef_method :==
def == x
raise "int \#{self} not \#{x}"
end
end
x = 3
puts "will raise"
begin
p [1, 2].include?(x)
rescue
puts $!
end
RUBY
end
def test_opt_newarray_send_include_p
[
'b = :b; [:a, b].include?(:b)',
# Use Object.new to ensure that we get newarray rather than duparray.
'value = 1; [Object.new, true, "true", 1].include?(value)',
'value = 1; [Object.new, "1"].include?(value.to_s)',
'[Object.new, "1"].include?(self) == false',
].each do |code|
iseq = RubyVM::InstructionSequence.compile(code)
insn = iseq.disasm
assert_match(/opt_newarray_send/, insn)
assert_no_match(/\bnewarray\b/, insn)
assert_equal(true, eval(code))
end
x, y = :b, :c
assert_equal(true, [:a, x].include?(x))
assert_equal(false, [:a, x].include?(y))
assert_in_out_err([], <<~RUBY, ["1,3", "3,3", "1,4", "4,4"])
class Array
prepend(Module.new do
def include?(i)
puts self.join(",")
# Modify self to prove that we are operating on a copy.
map! { i }
puts self.join(",")
end
end)
end
def x(i)
[1, i].include?(i)
end
x(3)
x(4)
RUBY
# Ensure raises happen correctly.
assert_in_out_err([], <<~RUBY, ["will raise", "int 1 not 3"])
class Integer
undef_method :==
def == x
raise "int \#{self} not \#{x}"
end
end
x = 3
puts "will raise"
begin
p [1, x].include?(x)
rescue
puts $!
end
RUBY
end
end