|
# 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 |
|
|