summaryrefslogtreecommitdiff
path: root/test/ruby
diff options
context:
space:
mode:
authorJeremy Evans <[email protected]>2024-08-19 19:00:37 -0700
committerGitHub <[email protected]>2024-08-19 19:00:37 -0700
commitea7ceff82cec98d0c419e9807dcb33dcc08b56fa (patch)
tree2292aa2fe8f97cd82bc68b3b546d7ae2abdb5226 /test/ruby
parent6dccb0131e109e480d6b86c98a14ce8e9f2cad4c (diff)
Avoid hash allocation for certain proc calls
Previously, proc calls such as: ```ruby proc{|| }.(**empty_hash) proc{|b: 1| }.(**r2k_array_with_empty_hash) ``` both allocated hashes unnecessarily, due to two separate code paths. The first call goes through CALLER_SETUP_ARG/vm_caller_setup_keyword_hash, and is simple to fix by not duping an empty keyword hash that will be dropped. The second case is more involved, in setup_parameters_complex, but is fixed the exact same way as when the ruby2_keywords hash is not empty, by flattening the rest array to the VM stack, ignoring the last element (the empty keyword splat). Add a flatten_rest_array static function to handle this case. Update test_allocation.rb to automatically convert the method call allocation tests to proc allocation tests, at least for the calls that can be converted. With the code changes, all proc call allocation tests pass, showing that proc calls and method calls now allocate the same number of objects. I've audited the allocation tests, and I believe that all of the low hanging fruit has been collected. All remaining allocations are either caller side: * Positional splat + post argument * Multiple positional splats * Literal keywords + keyword splat * Multiple keyword splats Or callee side: * Positional splat parameter * Keyword splat parameter * Keyword to positional argument conversion for methods that don't accept keywords * ruby2_keywords method called with keywords Reapplies abc04e898b627ab37fa9dd5e330f239768778d8b, which was reverted at d56470a27c5a8a2e7aee7a76cea445c2d29c0c59, with the addition of a bug fix and test. Fixes [Bug #20679]
Notes
Notes: Merged: https://github.com/ruby/ruby/pull/11409 Merged-By: jeremyevans <[email protected]>
Diffstat (limited to 'test/ruby')
-rw-r--r--test/ruby/test_allocation.rb77
-rw-r--r--test/ruby/test_keyword.rb14
2 files changed, 89 insertions, 2 deletions
diff --git a/test/ruby/test_allocation.rb b/test/ruby/test_allocation.rb
index 60faa44e4f..9ba01dfcf9 100644
--- a/test/ruby/test_allocation.rb
+++ b/test/ruby/test_allocation.rb
@@ -2,10 +2,16 @@
require 'test/unit'
class TestAllocation < Test::Unit::TestCase
+ def munge_checks(checks)
+ checks
+ end
+
def check_allocations(checks)
dups = checks.split("\n").reject(&:empty?).tally.select{|_,v| v > 1}
raise "duplicate checks:\n#{dups.keys.join("\n")}" unless dups.empty?
+ checks = munge_checks(checks)
+
assert_separately([], <<~RUBY)
$allocations = [0, 0]
$counts = {}
@@ -549,7 +555,8 @@ class TestAllocation < Test::Unit::TestCase
def test_nested_anonymous_splat_and_anonymous_keyword_splat_parameters
check_allocations(<<~RUBY)
- def self.anon_splat_and_anon_keyword_splat(*, **#{block}); t(*, **) end; def self.t(*, **#{block}); end
+ def self.t(*, **#{block}); end
+ def self.anon_splat_and_anon_keyword_splat(*, **#{block}); t(*, **) end
check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, a: 2#{block})")
check_allocations(1, 0, "anon_splat_and_anon_keyword_splat(1, *empty_array, a: 2#{block})")
@@ -639,7 +646,8 @@ class TestAllocation < Test::Unit::TestCase
def test_nested_argument_forwarding
check_allocations(<<~RUBY)
- def self.argument_forwarding(...); t(...) end; def self.t(...) end
+ def self.t(...) end
+ def self.argument_forwarding(...); t(...) end
check_allocations(0, 0, "argument_forwarding(1, a: 2#{block})")
check_allocations(0, 0, "argument_forwarding(1, *empty_array, a: 2#{block})")
@@ -766,4 +774,69 @@ class TestAllocation < Test::Unit::TestCase
end
end
end
+
+ class ProcCall < MethodCall
+ def munge_checks(checks)
+ return checks if @no_munge
+ sub = rep = nil
+ checks.split("\n").map do |line|
+ case line
+ when "singleton_class.send(:ruby2_keywords, :r2k)"
+ "r2k.ruby2_keywords"
+ when /\Adef self.([a-z0-9_]+)\((.*)\);(.*)end\z/
+ sub = $1 + '('
+ rep = $1 + '.('
+ "#{$1} = #{$1} = proc{ |#{$2}| #{$3} }"
+ when /check_allocations/
+ line.gsub(sub, rep)
+ else
+ line
+ end
+ end.join("\n")
+ end
+
+ # Generic argument forwarding not supported in proc definitions
+ undef_method :test_argument_forwarding
+ undef_method :test_nested_argument_forwarding
+
+ # Proc anonymous arguments cannot be used directly
+ undef_method :test_nested_anonymous_splat_and_anonymous_keyword_splat_parameters
+
+ def test_no_array_allocation_with_splat_and_nonstatic_keywords
+ @no_munge = true
+
+ check_allocations(<<~RUBY)
+ keyword = keyword = proc{ |a: nil, b: nil #{block}| }
+
+ check_allocations(0, 1, "keyword.(*empty_array, a: empty_array#{block})") # LVAR
+ check_allocations(0, 1, "->{keyword.(*empty_array, a: empty_array#{block})}.call") # DVAR
+ check_allocations(0, 1, "$x = empty_array; keyword.(*empty_array, a: $x#{block})") # GVAR
+ check_allocations(0, 1, "@x = empty_array; keyword.(*empty_array, a: @x#{block})") # IVAR
+ check_allocations(0, 1, "self.class.const_set(:X, empty_array); keyword.(*empty_array, a: X#{block})") # CONST
+ check_allocations(0, 1, "keyword.(*empty_array, a: Object::X#{block})") # COLON2
+ check_allocations(0, 1, "keyword.(*empty_array, a: ::X#{block})") # COLON3
+ check_allocations(0, 1, "T = keyword; #{'B = block' unless block.empty?}; class Object; @@x = X; T.(*X, a: @@x#{', &B' unless block.empty?}) end") # CVAR
+ check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: 1#{block})") # INTEGER
+ check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: 1.0#{block})") # FLOAT
+ check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: 1.0r#{block})") # RATIONAL
+ check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: 1.0i#{block})") # IMAGINARY
+ check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: 'a'#{block})") # STR
+ check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: :b#{block})") # SYM
+ check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: /a/#{block})") # REGX
+ check_allocations(0, 1, "keyword.(*empty_array, a: self#{block})") # SELF
+ check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: nil#{block})") # NIL
+ check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: true#{block})") # TRUE
+ check_allocations(0, 1, "keyword.(*empty_array, a: empty_array, b: false#{block})") # FALSE
+ check_allocations(0, 1, "keyword.(*empty_array, a: ->{}#{block})") # LAMBDA
+ check_allocations(0, 1, "keyword.(*empty_array, a: $1#{block})") # NTH_REF
+ check_allocations(0, 1, "keyword.(*empty_array, a: $`#{block})") # BACK_REF
+ RUBY
+ end
+
+ class WithBlock < self
+ def block
+ ', &block'
+ end
+ end
+ end
end
diff --git a/test/ruby/test_keyword.rb b/test/ruby/test_keyword.rb
index 7a9cd74b40..4c6c35e6c3 100644
--- a/test/ruby/test_keyword.rb
+++ b/test/ruby/test_keyword.rb
@@ -2848,6 +2848,20 @@ class TestKeywordArguments < Test::Unit::TestCase
assert_equal(1, process(:foo, bar: :baz))
end
+ def test_ruby2_keywords_bug_20679
+ c = Class.new do
+ def self.get(_, _, h, &block)
+ h[1]
+ end
+
+ ruby2_keywords def get(*args, &block)
+ self.class.get(*args, &block)
+ end
+ end
+
+ assert_equal 2, c.new.get(true, {}, 1 => 2)
+ end
+
def test_top_ruby2_keywords
assert_in_out_err([], <<-INPUT, ["[1, 2, 3]", "{:k=>1}"], [])
def bar(*a, **kw)