diff options
author | Jeremy Evans <[email protected]> | 2024-07-26 15:18:45 -0700 |
---|---|---|
committer | Jeremy Evans <[email protected]> | 2024-08-15 13:00:09 -0700 |
commit | abc04e898b627ab37fa9dd5e330f239768778d8b (patch) | |
tree | 50b62c2c3db7b399bc839f304bca2f7276dd08f9 /test | |
parent | 2c6e16eb51bccb98c2d4cfb7b35f6e6500d5d028 (diff) |
Avoid hash allocation for certain proc calls
Previous, 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
Notes
Notes:
Merged: https://github.com/ruby/ruby/pull/11258
Diffstat (limited to 'test')
-rw-r--r-- | test/ruby/test_allocation.rb | 77 |
1 files changed, 75 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 |