Fix crash when passing large keyword splat to method accepting keywords and keyword splat

The following code previously caused a crash:

```ruby
h = {}
1000000.times{|i| h[i.to_s.to_sym] = i}
def f(kw: 1, **kws) end
f(**h)
```

Inside a thread or fiber, the size of the keyword splat could be much smaller
and still cause a crash.

I found this issue while optimizing method calling by reducing implicit
allocations.  Given the following code:

```ruby
def f(kw: , **kws) end
kw = {kw: 1}
f(**kw)
```

The `f(**kw)` call previously allocated two hashes callee side instead of a
single hash.  This is because `setup_parameters_complex` would extract the
keywords from the keyword splat hash to the C stack, to attempt to mirror
the case when literal keywords are passed without a keyword splat.  Then,
`make_rest_kw_hash` would build a new hash based on the extracted keywords
that weren't used for literal keywords.

Switch the implementation so that if a keyword splat is passed, literal keywords
are deleted from the keyword splat hash (or a copy of the hash if the hash is
not mutable).

In addition to avoiding the crash, this new approach is much more
efficient in all cases.  With the included benchmark:

```
                                1
            miniruby:   5247879.9 i/s
     miniruby-before:   2474050.2 i/s - 2.12x  slower

                        1_mutable
            miniruby:   1797036.5 i/s
     miniruby-before:   1239543.3 i/s - 1.45x  slower

                               10
            miniruby:   1094750.1 i/s
     miniruby-before:    365529.6 i/s - 2.99x  slower

                       10_mutable
            miniruby:    407781.7 i/s
     miniruby-before:    225364.0 i/s - 1.81x  slower

                              100
            miniruby:    100992.3 i/s
     miniruby-before:     32703.6 i/s - 3.09x  slower

                      100_mutable
            miniruby:     40092.3 i/s
     miniruby-before:     21266.9 i/s - 1.89x  slower

                             1000
            miniruby:     21694.2 i/s
     miniruby-before:      4949.8 i/s - 4.38x  slower

                     1000_mutable
            miniruby:      5819.5 i/s
     miniruby-before:      2995.0 i/s - 1.94x  slower
```
This commit is contained in:
Jeremy Evans 2024-01-30 17:15:44 -08:00
parent 93accfdf48
commit c20e819e8b
3 changed files with 118 additions and 25 deletions

View file

@ -0,0 +1,25 @@
prelude: |
h1, h10, h100, h1000 = [1, 10, 100, 1000].map do |n|
h = {kw: 1}
n.times{|i| h[i.to_s.to_sym] = i}
h
end
eh = {}
def kw(kw: nil, **kws) end
benchmark:
1: |
kw(**h1)
1_mutable: |
kw(**eh, **h1)
10: |
kw(**h10)
10_mutable: |
kw(**eh, **h10)
100: |
kw(**h100)
100_mutable: |
kw(**eh, **h100)
1000: |
kw(**h1000)
1000_mutable: |
kw(**eh, **h1000)