Skip to content

Instantly share code, notes, and snippets.

@palkan
Last active April 7, 2019 23:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save palkan/974058570b364adcd13a3a37147a9488 to your computer and use it in GitHub Desktop.
Save palkan/974058570b364adcd13a3a37147a9488 to your computer and use it in GitHub Desktop.
[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