[Saint-P RubyConf 2018] RubyDispatch
# 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment