Avoid allocation for anonymous positional splat with no arguments

Anonymous positional splats cannot be directly accessed, they can
only be passed as splats to other methods.  So if an anonymous
positional splat would be empty, you can use a shared frozen
empty array to save an allocation.

```ruby
def a(*) end
a()
```

This is similar to how anonymous empty keyword splats are optimized,
except those use `nil` instead of a shared empty frozen hash.

This updates the allocation tests to check that the array allocations
are avoided where possible.

It also makes a small change to test_iseq.rb to ensure an unfrozen
hash is passed as the value of an anonymous splat parameter.
This commit is contained in:
Jeremy Evans 2025-01-18 08:54:28 -08:00
parent 72cb68972c
commit 43683e1e9d
Notes: git 2025-03-27 20:59:20 +00:00
3 changed files with 26 additions and 11 deletions

View File

@ -524,6 +524,7 @@ class TestAllocation < Test::Unit::TestCase
end
def test_anonymous_splat_and_anonymous_keyword_splat_parameters
only_block = block.empty? ? block : block[2..]
check_allocations(<<~RUBY)
def self.anon_splat_and_anon_keyword_splat(*, **#{block}); end
@ -556,6 +557,10 @@ class TestAllocation < Test::Unit::TestCase
check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*array1, *empty_array, a: 2, **empty_hash#{block})")
check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*array1, *empty_array, **hash1, **empty_hash#{block})")
check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(#{only_block})")
check_allocations(0, 1, "anon_splat_and_anon_keyword_splat(a: 2#{block})")
check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(**empty_hash#{block})")
check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, *empty_array, a: 2, **empty_hash#{block})")
check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, *empty_array, **hash1, **empty_hash#{block})")
check_allocations(0, 1, "anon_splat_and_anon_keyword_splat(*array1, **empty_hash, a: 2#{block})")
@ -570,6 +575,7 @@ class TestAllocation < Test::Unit::TestCase
end
def test_nested_anonymous_splat_and_anonymous_keyword_splat_parameters
only_block = block.empty? ? block : block[2..]
check_allocations(<<~RUBY)
def self.t(*, **#{block}); end
def self.anon_splat_and_anon_keyword_splat(*, **#{block}); t(*, **) end
@ -603,6 +609,10 @@ class TestAllocation < Test::Unit::TestCase
check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*array1, *empty_array, a: 2, **empty_hash#{block})")
check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(*array1, *empty_array, **hash1, **empty_hash#{block})")
check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(#{only_block})")
check_allocations(0, 1, "anon_splat_and_anon_keyword_splat(a: 2#{block})")
check_allocations(0, 0, "anon_splat_and_anon_keyword_splat(**empty_hash#{block})")
check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, *empty_array, a: 2, **empty_hash#{block})")
check_allocations(1, 1, "anon_splat_and_anon_keyword_splat(1, *empty_array, **hash1, **empty_hash#{block})")
check_allocations(0, 1, "anon_splat_and_anon_keyword_splat(*array1, **empty_hash, a: 2#{block})")

View File

@ -173,7 +173,7 @@ class TestISeq < Test::Unit::TestCase
obj = Object.new
def obj.foo(*) nil.instance_eval{ ->{super} } end
assert_raise_with_message(Ractor::IsolationError, /refer unshareable object \[\] from variable '\*'/) do
Ractor.make_shareable(obj.foo)
Ractor.make_shareable(obj.foo(*[]))
end
end

View File

@ -894,16 +894,21 @@ setup_parameters_complex(rb_execution_context_t * const ec, const rb_iseq_t * co
}
if (ISEQ_BODY(iseq)->param.flags.has_rest) {
args_setup_rest_parameter(args, locals + ISEQ_BODY(iseq)->param.rest_start);
VALUE ary = *(locals + ISEQ_BODY(iseq)->param.rest_start);
VALUE index = RARRAY_LEN(ary) - 1;
if (splat_flagged_keyword_hash &&
!ISEQ_BODY(iseq)->param.flags.ruby2_keywords &&
!ISEQ_BODY(iseq)->param.flags.has_kw &&
!ISEQ_BODY(iseq)->param.flags.has_kwrest &&
RARRAY_AREF(ary, index) == splat_flagged_keyword_hash) {
((struct RHash *)rest_last)->basic.flags &= ~RHASH_PASS_AS_KEYWORDS;
RARRAY_ASET(ary, index, rest_last);
if (UNLIKELY(ISEQ_BODY(iseq)->param.flags.anon_rest && args->argc == 0 && !args->rest && !ISEQ_BODY(iseq)->param.flags.has_post)) {
*(locals + ISEQ_BODY(iseq)->param.rest_start) = args->rest = rb_cArray_empty_frozen;
}
else {
args_setup_rest_parameter(args, locals + ISEQ_BODY(iseq)->param.rest_start);
VALUE ary = *(locals + ISEQ_BODY(iseq)->param.rest_start);
VALUE index = RARRAY_LEN(ary) - 1;
if (splat_flagged_keyword_hash &&
!ISEQ_BODY(iseq)->param.flags.ruby2_keywords &&
!ISEQ_BODY(iseq)->param.flags.has_kw &&
!ISEQ_BODY(iseq)->param.flags.has_kwrest &&
RARRAY_AREF(ary, index) == splat_flagged_keyword_hash) {
((struct RHash *)rest_last)->basic.flags &= ~RHASH_PASS_AS_KEYWORDS;
RARRAY_ASET(ary, index, rest_last);
}
}
}