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>