Project

General

Profile

Actions

Feature #20066

closed

Reduce Implicit Array/Hash Allocations For Method Calls Involving Splats

Added by jeremyevans0 (Jeremy Evans) 5 months ago. Updated 4 months ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:115749]

Description

I have submitted a pull request (https://github.com/ruby/ruby/pull/9247) to reduce implicit array and hash allocations for method calls involving splats. The following optimizations are included:

VM_CALL_ARGS_SPLAT_MUT callinfo flag

This is similar to the VM_CALL_KW_SPLAT_MUT flag added in Ruby 3.0. This makes it so if the caller-side allocates an array for the method call, the flag is used to signal to the callee that it can reuse the allocated array and does not need to duplicate it.

concattoarray VM instruction

This instruction is similar to concatarray, but assumes the object being concatenated to is already a mutable array (such as those created by the splatarray VM instruction). This optimizes method calls with multiple splats such as f(*a,*a,*a) (which previously allocated 3 arrays), allocating a single array instead of an array per splatted array.

pushtoarray VM instruction

This is similar, but handles non-splat arguments after a splat. Previously, the VM would wrap those arguments in an array using newarray, and then call concatarray, such that f(*a, a) allocated 3 arrays caller-side. This instruction just appends to the mutable array, reducing the number of arrays allocated to 1.

Allocationless Anonymous Splat Forwarding

This allows def f(*, **) end to not allocate an array or hash callee side. This works because it is not possible to mutate the local variables, only pass them as splats to other methods. This can make the following call chain allocation less:

def f(a, b: 1) end
def g(*, **) f(*, **) end
ea

a = [1]
kw = {b: 2}
g(*a, **kw) # No allocations in this call

Switch ... argument forwards to not use ruby2_keywords

Using ruby2_keywords has probably been slower since Koichi's changes early in the Ruby 3.3 development cycle to not combine keyword splats into the positional splat array. This removes the FORWARD_ARGS_WITH_RUBY2_KEYWORDS define, so that def f(...) end operates similarly to def f(*, **) end, allowing allocationless splat forwarding

Reduce array and hash allocations for nested argument forwarding calls

This uses a combination of frame flags and callinfo flags to track mutability of anonymous splat variables. It can make it so the following call example only allocates a 1 array and 1 hash:

def m1(*args, **kw)
end

def m2(...)
  m1(...)
end

def m3(*, **)
  m2(*, **)
end

m3(1, a: 1) # 1 array and 1 hash allocated

In the above example, the call to m3 allocates an array ([1]) and a hash ({a: 1}), but the call to m2 passes them as mutable splats, m2 treats them as mutable splats when calling m1, and m1 reuses the array that m3 allocated for args and the hash that m3 allocated for kw.

I created a benchmark for all of these changes. In the method calls optimized by these changes, it is significantly faster:

named_multi_arg_splat
after:   5344097.6 i/s 
before:   3088134.0 i/s - 1.73x  slower

named_post_splat
after:   5401882.3 i/s 
before:   2629321.8 i/s - 2.05x  slower

anon_arg_splat
after:  12242780.9 i/s 
before:   6845413.2 i/s - 1.79x  slower

anon_arg_kw_splat
after:  11277398.7 i/s 
before:   4329509.4 i/s - 2.60x  slower

anon_multi_arg_splat
after:   5132699.5 i/s 
before:   3018103.7 i/s - 1.70x  slower

anon_post_splat
after:   5602915.1 i/s 
before:   2645185.5 i/s - 2.12x  slower

anon_kw_splat
after:  15403727.3 i/s 
before:   6249504.6 i/s - 2.46x  slower

anon_fw_to_named_splat
after:   2985715.3 i/s 
before:   2049159.9 i/s - 1.46x  slower

anon_fw_to_named_no_splat
after:   2941030.4 i/s 
before:   2100380.0 i/s - 1.40x  slower

fw_to_named_splat
after:   2801008.7 i/s 
before:   2012416.4 i/s - 1.39x  slower

fw_to_named_no_splat
after:   2742670.4 i/s 
before:   1957707.2 i/s - 1.40x  slower

fw_to_anon_to_named_splat
after:   2309246.6 i/s 
before:   1375924.6 i/s - 1.68x  slower

fw_to_anon_to_named_no_splat
after:   2193227.6 i/s 
before:   1351184.1 i/s - 1.62x  slower

Only fallout from these changes:

  • Minor change to AST output for ... not using ruby2_keywords
  • Prism and rbs need updating for ... not using ruby2_keywords
  • typeprof need updating for new VM instructions (at least pushtoarray)

VM_CALL_ARGS_SPLAT_MUT, concattoarray, and pushtoarray only affect uncommon callsites (multiple splats, argument after splat). Other commits only optimize calls to methods using anonymous splats or ... argument forwarding. Previously, there was no performance reason to use anonymous splats or ... argument forwarding, but with this change, using them can be faster, and can offer a new way for users to optimize their code.

In my opinion, this is too late for consideration in Ruby 3.3, but it could be considered for Ruby 3.4.

Actions

Also available in: Atom PDF

Like1
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0