[#98645] [Ruby master Misc#16933] DevelopersMeeting20200618Japan — mame@...

Issue #16933 has been reported by mame (Yusuke Endoh).

14 messages 2020/06/04

[#98663] [Ruby master Bug#16936] `make check TESTS="-n !/Foo#method/"` not skipping the test case — jaruga@...

Issue #16936 has been reported by jaruga (Jun Aruga).

13 messages 2020/06/05

[#98772] [Ruby master Bug#16959] Weakmap has specs and third-party usage despite being a private API — headius@...

Issue #16959 has been reported by headius (Charles Nutter).

13 messages 2020/06/12

[#98826] [Ruby master Feature#16963] Remove English.rb from Ruby 2.8/3.0 — hsbt@...

Issue #16963 has been reported by hsbt (Hiroshi SHIBATA).

9 messages 2020/06/16

[#98920] [Ruby master Bug#16978] Ruby should not use realpath for __FILE__ — v.ondruch@...

Issue #16978 has been reported by vo.x (Vit Ondruch).

24 messages 2020/06/23

[#98947] [Ruby master Feature#16986] Anonymous Struct literal — ko1@...

Issue #16986 has been reported by ko1 (Koichi Sasada).

66 messages 2020/06/26

[#98964] [Ruby master Feature#16989] Sets: need ♥️ — marcandre-ruby-core@...

Issue #16989 has been reported by marcandre (Marc-Andre Lafortune).

33 messages 2020/06/26

[#98965] [Ruby master Feature#16990] Sets: operators compatibility with Array — marcandre-ruby-core@...

Issue #16990 has been reported by marcandre (Marc-Andre Lafortune).

11 messages 2020/06/26

[#98968] [Ruby master Feature#16993] Sets: from hash keys using Hash#key_set — marcandre-ruby-core@...

Issue #16993 has been reported by marcandre (Marc-Andre Lafortune).

10 messages 2020/06/26

[#98997] [Ruby master Feature#17000] 2.7.2 turns off deprecation warnings by deafult — mame@...

Issue #17000 has been reported by mame (Yusuke Endoh).

16 messages 2020/06/30

[ruby-core:98959] [Ruby master Feature#16897] General purpose memoizer in Ruby 3 with Ruby 2 performance

Date: 2020-06-26 15:51:31 UTC
List: ruby-core #98959
Issue #16897 has been updated by Dan0042 (Daniel DeLorme).


sam.saffron (Sam Saffron) wrote in #note-28:
> If we keep `foo(a: 1)` working a developer may get confused (despite everything operating 100% correctly):
> 
> > Why does `foo(a: 1)` work and `bar(*[{a: 1}])` not work? 
> 
> I still think that wider compatibility is better, teaching the developer about this trick for the huge edge case imo is better than the alternative of dropping support for kwarg tracking in `*args`: 
> 
> ```
> h = {a: 1}
> h.kwargs! 
> bar(*[h])
> ```

I feel compelled to make a quick note here that what Sam is saying above is very similar to what I proposed in #16511, with a few differences:

```ruby
foo(a: 1)      #keyword 'a'
bar(*[{a: 1}]) #positional hash {a: 1}
bar(*[a: 1])   #keyword 'a'
h = {a: 1}
bar(*[**h])    #keyword 'a'
```

Although I disagree this has to be carried long term; mid term is good enough to facilitate migration greatly, and may even give enough time to come up with a good memoizer solution.

----------------------------------------
Feature #16897: General purpose memoizer in Ruby 3 with Ruby 2 performance
https://bugs.ruby-lang.org/issues/16897#change-86334

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

module Memoizer
def memoize_26(method_name)
  cache = {}

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

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

  def memoize_27(method_name)
    cache = {}

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

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

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

    cache = "MEMOIZE_#{method_name}"

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

    args = []

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

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

    class_eval <<~RUBY
      #{cache} = {}
      def #{method_name}(#{args_text})
        found = true
        all_args = #{args.length === 2 ? "[args, kwargs]" : args[0]}
        data = #{cache}.fetch(all_args) { found = false }
        unless found
          #{cache}[all_args] = data = public_send(:#{uncached} #{args.empty? ? "" : ", #{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 = [
  OldMethod.new,
  NewMethod.new,
  OptimizedMethod.new
]

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

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

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

    x.report("#{m.class} args and kwargs") do |times|
      while times > 0
        m.args_and_kwargs(10, b: 10)
        times -= 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  (0.00) slower
# OldMethod args and kwargs:   940911.3 i/s - 1.09x  (ア 0.00) slower
#  OldMethod args only:   930446.1 i/s - 1.10x  (ア 0.00) slower
# OldMethod kwargs only:   858238.5 i/s - 1.19x  (ア 0.00) slower
# OptimizedMethod args and kwargs:   773773.5 i/s - 1.32x  (ア 0.00) slower
# NewMethod args and kwargs:   772653.3 i/s - 1.32x  (ア 0.00) slower
#  NewMethod args only:   771253.2 i/s - 1.32x  (ア 0.00) slower
# NewMethod kwargs only:   700604.1 i/s - 1.46x  (ア 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]] += 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] += 1
  orig_count(*args, **kwargs)
end
```

I would like to propose some changes to Ruby 3 to allow to recover this performance. 

Perhaps:

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

Or:

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

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

Thoughts? 



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

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

In This Thread

Prev Next