Issue #16188 has been updated by Eregon (Benoit Daloze).


jeremyevans0 (Jeremy Evans) wrote:
> > * Remove `ruby2_keywords` in Ruby 3.0, just have it in Ruby 2.7 where it's needed. Ruby 3.0 (with the keyword arg separation) doesn't need `ruby2_keywords`.
> 
> `ruby2_keywords` is about 2x faster than explicit keyword arguments in CRuby, so this would actually decrease performance in CRuby.

I don't think anyone is seriously considering using `ruby2_keywords` in Ruby 3+ (or at least in Ruby 3.3+).
So it seems CRuby should improve performance for `*args, **kwargs`, or propose another way to delegate more efficiently (e.g., `...` is one way I think many Rubyists would like).

> > * Combine `ruby2_keywords` with `send_keyword_hash`, which solves the performance issue and is explicit, therefore improving readability and debug-ability.
> 
> This requires modifying the internals of methods instead of just flagging the methods, and is much more invasive to the user.

It requires extra modifications, yes, but IMHO very easy modifications: marking where positional args should be converted to kwargs for delegation.
Since users have to think where they need `ruby2_keywords`, I would argue placing `send_keyword_hash` is no harder than adding `ruby2_keywords`, and makes the whole model a lot easier to understand, less magic and fixes performance.

> > * Use another way for delegation in Ruby 2.7 (e.g. the lexical `pass_keywords` or `...`, see https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html )
> 
> The lexical `pass_keywords` is not truly lexical, as only the current VM frame was flagged, so the behavior inside blocks in a method was not what the user would expect.  Modifying the implementation to handle lexical VM frames could possibly result in more slowdown, and I'm not sure how to implement it.

I do mean lexical, including calls inside blocks.

I would think it's straightforward to implement `pass_keywords`, including blocks, without overhead on unrelated code:
Modify every call site inside `pass_keywords` methods with `*args`, including those in blocks inside the method, to use a different bytecode.
In that bytecode's implementation, do the extra logic needed.
We might also need some prelude logic in the marked method to remember if it was passed kwargs.
And that's all there is to it, isn't it?

> Additionally, there are cases where non-lexical passing is used (e.g. in Rails), and a lexical approach would not handle those cases.

I showed it can be done with a block: https://github.com/eregon/rails/commit/8b0625ed68

> `ruby2_keywords` handles that case, and many other real world cases that the lexical approach does not handle.

Can you give an example the above approach (capturing the call inside the lambda) cannot handle?

> `...` doesn't handle all delegation cases, it only handles a subset where all arguments are passed and no arguments are added/removed/changed.  There are many cases where it cannot be used.

Yes, we should at least allow leading arguments as that's so frequent.
I'll file an issue for it if there isn't one already.
I believe everyone expects `def m(a, ...)` to work.

I think `...` is actually what most Rubyists want.
I think the only real difficulty with `...` is how to use in older Ruby versions (`eval` is one possibility).
Maybe we should backport `...` to 2.4/2.5/2.6.

----------------------------------------
Misc #16188: What are the performance implications of the new keyword arguments in 2.7 and 3.0?
https://bugs.ruby-lang.org/issues/16188#change-82774

* Author: Eregon (Benoit Daloze)
* Status: Open
* Priority: Normal
* Assignee: jeremyevans0 (Jeremy Evans)
----------------------------------------
In #14183, keyword arguments became further separated from positional arguments.

Contrary to the original design though, keyword and positional arguments are not fully separated for methods not accepting keyword arguments.
Example: `foo(key: :value)` will `def foo(hash)` will pass a positional argument.
This is of course better for compatibility, but I wonder what are the performance implications.

The block argument is completely separate in all versions, so no need to concern ourselves about that.

In Ruby <= 2.6:
* The caller never needs to know about the callee's arguments, it can just take all arguments and pass them as an array.
  The last argument might be used to extract keyword, but this is all done at the callee side.
* Splitting kwargs composed of Symbol and non-Symbol keys can be fairly expensive, but it is a rare occurrence.
  If inlining the callee and kwargs are all passed as a literal Hash at the call site, there shouldn't be any overhead compared to positional arguments once JIT'ed.

In Ruby 2.7:
* The caller needs to pass positional and keyword arguments separately, at least when calling a method accepting kwargs.
  But, if it calls a methods not accepting kwargs, then the "kwargs" (e.g. `foo(key: :value)`) should be treated just like a final Hash positional argument.
* (If we had complete separation, then we could always pass positional and keyword arguments separately, so the caller could once again ignore the callee)

How is the logic implemented in MRI for 2.7?

Specializing the caller for a given callee is a well-known technique.
However, it becomes more difficult if different methods are called from the same callsite (polymorphic call), especially if one accepts kwargs and another does not.
In that case, I think we will see a performance cost to this approach, by having to pass arguments differently based on the method to be called.

What about delegation using `ruby2_keywords`?
Which checks does that add (compared to 2.6) in the merged approach with the Hash flag?



-- 
https://bugs.ruby-lang.org/

Unsubscribe: <mailto:ruby-core-request / ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>