| # Pure Ruby (partial) implementation of CRuby method dispatch. | |
| # | |
| # How to run benchmarks: | |
| # | |
| # $ BENCH=1 ruby ruby_dispatch.rb | |
| # | |
| module RubyDispatch | |
| MISSING = :method_missing | |
| # OPT_GLOBAL_METHOD_CACHE: https://github.com/ruby/ruby/blob/ruby_2_5/vm_method.c#L9 | |
| CACHE = Hash.new { |h, k| h[k] = {} } | |
| CACHE_STAT = { hit: 0, miss: 0 } | |
| at_exit { p CACHE_STAT } | |
| refine BasicObject do | |
| # opt_send_without_block: https://github.com/ruby/ruby/blob/v2_5_1/insns.def#L907 | |
| def rd_send(mid, *args) | |
| meth = rd_search_method(self.class, mid) | |
| # vm_call_method: https://github.com/ruby/ruby/blob/v2_5_1/vm_insnhelper.c#L2391 | |
| if meth.nil? | |
| meth = rd_search_method(self.class, MISSING) | |
| args.unshift(mid) | |
| end | |
| # vm_call_method: https://github.com/ruby/ruby/blob/v2_5_1/vm_insnhelper.c#L2353 | |
| rd_call_method(self, meth, *args) | |
| end | |
| # vm_call_method_nome: https://github.com/ruby/ruby/blob/v2_5_1/vm_insnhelper.c#L2330 | |
| def rd_call_no_method(obj, mid, args) | |
| # vm_call_method_missing: https://github.com/ruby/ruby/blob/v2_5_1/vm_insnhelper.c#L2069 | |
| meth = rd_search_method(obj.class, MISSING) | |
| args.unshift(mid) | |
| rd_call_method(obj, meth, args) | |
| end | |
| def rd_call_method(obj, meth, *args) | |
| meth.bind(obj).call(*args) | |
| end | |
| # vm_search_method: https://github.com/ruby/ruby/blob/v2_5_1/vm_insnhelper.c#L1296 | |
| def rd_search_method(klass, mid) | |
| if ENV['CACHE'] == '1' && CACHE[klass].key?(mid) | |
| CACHE_STAT[:hit] += 1 | |
| return CACHE[klass][mid] | |
| end | |
| CACHE_STAT[:miss] += 1 | |
| # search_method: https://github.com/ruby/ruby/blob/v2_5_1/vm_method.c#L717 | |
| iter = klass.ancestors.each | |
| kl = klass | |
| loop do | |
| if kl.instance_methods(false).include?(mid) || | |
| kl.private_instance_methods(false).include?(mid) | |
| return CACHE[klass][mid] = kl.instance_method(mid) | |
| end | |
| if kl.eql?(BasicObject) | |
| return CACHE[klass][mid] = nil | |
| end | |
| kl = iter.next | |
| end | |
| end | |
| end | |
| # rb_clear_method_cache_by_class: https://github.com/ruby/ruby/blob/ruby_2_5/vm_method.c#L90 | |
| Module.prepend(Module.new do | |
| %w[append_features prepend_features].each do |mid| | |
| module_eval <<~SRC | |
| def #{mid}(base) | |
| CACHE.delete(base) | |
| super | |
| end | |
| SRC | |
| end | |
| %w[method_added method_removed method_undefined].each do |mid| | |
| module_eval <<~SRC | |
| def #{mid}(_) | |
| CACHE.delete(self) | |
| super | |
| end | |
| SRC | |
| end | |
| end) | |
| end | |
| # Test and benchmark | |
| require "benchmark" | |
| GC.disable | |
| using RubyDispatch | |
| class A | |
| def foo | |
| :foo | |
| end | |
| def method_missing(mid) | |
| mid | |
| end | |
| end | |
| a = A.new | |
| p a.rd_send(:foo) | |
| p a.rd_send(:bar) | |
| begin | |
| ?a.rd_send(:unknown) | |
| rescue NoMethodError => e | |
| p e.message | |
| end | |
| A.define_method(:foo) { false } | |
| p a.rd_send(:foo) | |
| exit(0) unless ENV['BENCH'] | |
| N = 1_000 | |
| methods = 1.upto(N).map { |n| :"foo#{n}" } | |
| Benchmark.bm(25) do |x| | |
| x.report("defined") do | |
| methods.each { a.rd_send(:foo) } | |
| end | |
| x.report("defined cached") do | |
| ENV['CACHE'] = '1' | |
| methods.each { a.rd_send(:foo) } | |
| ENV['CACHE'] = '0' | |
| end | |
| x.report("missing") do | |
| methods.each { |_| a.rd_send(:bar) } | |
| end | |
| x.report("missing cached") do | |
| ENV['CACHE'] = '1' | |
| methods.each { |_| a.rd_send(:bar) } | |
| ENV['CACHE'] = '0' | |
| end | |
| end | |