Issue #16897 has been updated by jeremyevans0 (Jeremy Evans).


sam.saffron (Sam Saffron) wrote in #note-11:
> That is relying on `Hash.ruby2_keywords_hash?` surely a long term solutio=
n for Ruby 3 can not rely on that?

`Hash.ruby2_keywords_hash?` will exist as long as `ruby2_keywords` does.  P=
ersonally, I don't think `ruby2_keywords` should be removed until an approa=
ch with equal or better performance is found.
 =

> At a minimum if this is the direction Ruby is taking (not expanding behav=
ior of ... and not fixing *args to be of type Arguments) there should be a =
clean interface without the word ruby2 in it.

Expanding behavior of `...` to support leading arguments has already been a=
pproved by matz and should be part of Ruby 3.  See #16378.

FWIW, the original name of `ruby2_keywords` was `pass_keywords`.  It was on=
ly renamed to `ruby2_keywords` precisely because the idea was it would be r=
emoved.

> ```
> def foo(*args)
>   puts args.kwargs?
> end
> =

> foo(a: 1)
> =3D> true
> =

> foo({a: 1})
> =3D> false
> ```

Already possible, assuming `ruby_keywords :foo`:

```ruby
class Array
  def kwargs?
    Hash =3D=3D=3D last && hash.ruby2_keywords_hash?(last)    =

  end
end
```

> My concern here is long term, what is the long term solution to this prob=
lem without using various `ruby2_XYZ` methods?

In Ruby 3, you can use `(*args, **kw)` delegation and it will work in all c=
ases.  It may not perform as well, though I have optimized `**kw` when call=
ing so that it never allocates more than 1 hash, and in some cases can be a=
llocation less.  See commit:d2c41b1bff1f3102544bb0d03d4e82356d034d33.  You =
should try benchmarking on the master branch if you are concerned about lon=
g term solutions and not about 2.7.

----------------------------------------
Feature #16897: Can a Ruby 3.0 compatible general purpose memoizer be writt=
en in such a way that it matches Ruby 2 performance?
https://bugs.ruby-lang.org/issues/16897#change-85979

* Author: sam.saffron (Sam Saffron)
* Status: Open
* Priority: Normal
----------------------------------------
```ruby
require 'benchmark/ips'

module Memoizer
def memoize_26(method_name)
  cache =3D {}

  uncached =3D "#{method_name}_without_cache"
  alias_method uncached, method_name

  define_method(method_name) do |*arguments|
    found =3D true
    data =3D cache.fetch(arguments) { found =3D false }
    unless found
      cache[arguments] =3D data =3D public_send(uncached, *arguments)
    end
    data
  end
end

  def memoize_27(method_name)
    cache =3D {}

    uncached =3D "#{method_name}_without_cache"
    alias_method uncached, method_name

    define_method(method_name) do |*args, **kwargs|
      found =3D true
      all_args =3D [args, kwargs]
      data =3D cache.fetch(all_args) { found =3D false }
      unless found
        cache[all_args] =3D data =3D public_send(uncached, *args, **kwargs)
      end
      data
    end
  end

  def memoize_27_v2(method_name)
    uncached =3D "#{method_name}_without_cache"
    alias_method uncached, method_name

    cache =3D "MEMOIZE_#{method_name}"

    params =3D instance_method(method_name).parameters
    has_kwargs =3D params.any? {|t, name| "#{t}".start_with? "key"}
    has_args =3D params.any? {|t, name| !"#{t}".start_with? "key"}

    args =3D []

    args << "args" if has_args
    args << "kwargs" if has_kwargs

    args_text =3D args.map do |n|
      n =3D=3D "args" ? "*args" : "**kwargs"
    end.join(",")

    class_eval <<~RUBY
      #{cache} =3D {}
      def #{method_name}(#{args_text})
        found =3D true
        all_args =3D #{args.length =3D=3D=3D 2 ? "[args, kwargs]" : args[0]}
        data =3D #{cache}.fetch(all_args) { found =3D false }
        unless found
          #{cache}[all_args] =3D data =3D public_send(:#{uncached} #{args.e=
mpty? ? "" : ", #{args_text}"})
        end
        data
      end
    RUBY

  end

end

module Methods
  def args_only(a, b)
    sleep 0.1
    "#{a} #{b}"
  end

  def kwargs_only(a:, b: nil)
    sleep 0.1
    "#{a} #{b}"
  end

  def args_and_kwargs(a, b:)
    sleep 0.1
    "#{a} #{b}"
  end
end

class OldMethod
  extend Memoizer
  include Methods

  memoize_26 :args_and_kwargs
  memoize_26 :args_only
  memoize_26 :kwargs_only
end

class NewMethod
  extend Memoizer
  include Methods

  memoize_27 :args_and_kwargs
  memoize_27 :args_only
  memoize_27 :kwargs_only
end

class OptimizedMethod
  extend Memoizer
  include Methods

  memoize_27_v2 :args_and_kwargs
  memoize_27_v2 :args_only
  memoize_27_v2 :kwargs_only
end

OptimizedMethod.new.args_only(1,2)


methods =3D [
  OldMethod.new,
  NewMethod.new,
  OptimizedMethod.new
]

Benchmark.ips do |x|
  x.warmup =3D 1
  x.time =3D 2

  methods.each do |m|
    x.report("#{m.class} args only") do |times|
      while times > 0
        m.args_only(10, b: 10)
        times -=3D 1
      end
    end

    x.report("#{m.class} kwargs only") do |times|
      while times > 0
        m.kwargs_only(a: 10, b: 10)
        times -=3D 1
      end
    end

    x.report("#{m.class} args and kwargs") do |times|
      while times > 0
        m.args_and_kwargs(10, b: 10)
        times -=3D 1
      end
    end
  end

  x.compare!
end


# # Ruby 2.6.5
# #
# OptimizedMethod args only:   974266.9 i/s
#  OldMethod args only:   949344.9 i/s - 1.03x  slower
# OldMethod args and kwargs:   945951.5 i/s - 1.03x  slower
# OptimizedMethod kwargs only:   939160.2 i/s - 1.04x  slower
# OldMethod kwargs only:   868229.3 i/s - 1.12x  slower
# OptimizedMethod args and kwargs:   751797.0 i/s - 1.30x  slower
#  NewMethod args only:   730594.4 i/s - 1.33x  slower
# NewMethod args and kwargs:   727300.5 i/s - 1.34x  slower
# NewMethod kwargs only:   665003.8 i/s - 1.47x  slower
#
# #
# # Ruby 2.7.1
#
# OptimizedMethod kwargs only:  1021707.6 i/s
# OptimizedMethod args only:   955694.6 i/s - 1.07x  (=B1 0.00) slower
# OldMethod args and kwargs:   940911.3 i/s - 1.09x  (=B1 0.00) slower
#  OldMethod args only:   930446.1 i/s - 1.10x  (=B1 0.00) slower
# OldMethod kwargs only:   858238.5 i/s - 1.19x  (=B1 0.00) slower
# OptimizedMethod args and kwargs:   773773.5 i/s - 1.32x  (=B1 0.00) slower
# NewMethod args and kwargs:   772653.3 i/s - 1.32x  (=B1 0.00) slower
#  NewMethod args only:   771253.2 i/s - 1.32x  (=B1 0.00) slower
# NewMethod kwargs only:   700604.1 i/s - 1.46x  (=B1 0.00) slower
```

The bottom line is that a generic delegator often needs to make use of all =
the arguments provided to a method.

```ruby
def count(*args, **kwargs)
  counter[[args, kwargs]] +=3D 1
  orig_count(*args, **kwargs)
end
```

The old pattern meant we could get away with one less array allocation per:

```ruby
def count(*args)
  counter[args] +=3D 1
  orig_count(*args, **kwargs)
end
```

I would like to propose some changes to Ruby 3 to allow to recover this per=
formance. =


Perhaps:

```ruby
def count(...)
  args =3D ...
  counter[args] +=3D 1
  orig_count(...)
end
```

Or:

```ruby
def count(***args)

  counter[args] +=3D 1
  orig_count(***args)
end
```

Thoughts? =




-- =

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

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