Ruby Method Overloading
require "benchmark/ips" | |
require_relative "./method_overloading" | |
class Foo | |
include MethodOverloading | |
def call(number) | |
"foo #{number}" | |
end | |
end | |
class Bar | |
def call(number) | |
"bar #{number}" | |
end | |
end | |
foo = Foo.new | |
bar = Bar.new | |
Benchmark.ips do |x| | |
x.report("method overloading") { foo.call(23) } | |
x.report("method") { bar.call(23) } | |
x.compare! | |
end | |
__END__ | |
Warming up -------------------------------------- | |
method overloading 24.146k i/100ms | |
method 305.480k i/100ms | |
Calculating ------------------------------------- | |
method overloading 225.254k (±13.5%) i/s - 1.111M in 5.053180s | |
method 3.274M (± 3.5%) i/s - 16.496M in 5.045562s | |
Comparison: | |
method: 3273590.9 i/s | |
method overloading: 225253.9 i/s - 14.53x (± 0.00) slower |
module MethodOverloading | |
def self.included(klass) | |
klass.class_eval do | |
@__method_overloading = {} | |
def self.method_added(method_name) | |
m = instance_method(method_name) | |
method_id = [method_name, m.arity] | |
@__method_overloading[method_id] = m | |
undef_method method_name | |
end | |
def self.respond_to_matching?(method_name, *args) | |
@__method_overloading.key?([method_name, args.count]) | |
end | |
def self.matched_call(instance, method_name, *args, &blk) | |
m = @__method_overloading[[method_name, args.count]] | |
m.bind_call(instance, *args) | |
end | |
end | |
end | |
def method_missing(method_name, *args, &blk) | |
super unless self.class.respond_to_matching?(method_name, *args, &blk) | |
self.class.matched_call(self, method_name, *args, &blk) | |
end | |
def respond_to_missing?(method_name, *) | |
self.class.respond_to_method?(method_name) | |
end | |
end |
This comment has been minimized.
This comment has been minimized.
thanks, didn't know about |
This comment has been minimized.
This comment has been minimized.
Got it down below 9x, will play with other ideas: require "benchmark/ips"
module MethodOverloading
def self.included(klass)
klass.class_eval do
@_matches = {}
@_methods = {}
@_recurse_catch = false
def self.method_added(method_name)
if @_recurse_catch
@_recurse_catch = false
return
end
original_method = instance_method(method_name)
@_matches[[method_name, original_method.arity]] = original_method
undef_method method_name
# Prevent recursive calls to method_added if we're doing it
# intentionally here.
@_recurse_catch = true
# Localize for closure
matches = @_matches
if @_methods[method_name]
define_method(method_name, @_methods[method_name])
else
define_method(method_name) do |*as, &fn|
match = matches[[method_name, as.count]]
match.bind(self).call(*as, &fn)
end
@_methods[method_name] = instance_method(method_name)
end
end
end
end
end
class Foo
include MethodOverloading
def call(number)
"foo #{number}"
end
def call
"foo 42"
end
end
class Bar
def call(number)
"bar #{number}"
end
end
foo = Foo.new
bar = Bar.new
Benchmark.ips do |x|
x.report("method overloading") { foo.call(23) }
x.report("method") { bar.call(23) }
x.compare!
end
# Warming up --------------------------------------
# method overloading 57.941k i/100ms
# method 252.803k i/100ms
# Calculating -------------------------------------
# method overloading 641.658k (± 8.7%) i/s - 3.187M in 5.005621s
# method 5.631M (± 3.4%) i/s - 28.314M in 5.034059s
# Comparison:
# method: 5631177.7 i/s
# method overloading: 641657.8 i/s - 8.78x slower |
This comment has been minimized.
This comment has been minimized.
Got it to 3.3x with a nested hash for matches using require "benchmark/ips"
module MethodOverloading
def self.included(klass)
klass.class_eval do
@_matches = Hash.new { |h, k| h[k] = {} }
@_methods = {}
@_recurse_catch = false
def self.method_added(method_name)
if @_recurse_catch
@_recurse_catch = false
return
end
original_method = instance_method(method_name)
@_matches[method_name][original_method.arity] = original_method
undef_method method_name
# Prevent recursive calls to method_added if we're doing it
# intentionally here.
@_recurse_catch = true
# Localize for closure
method_matches = @_matches[method_name]
if @_methods[method_name]
define_method(method_name, @_methods[method_name])
else
define_method(method_name) do |*as|
method_matches[as.size].bind(self).call(*as)
end
@_methods[method_name] = instance_method(method_name)
end
end
end
end
end
class Foo
include MethodOverloading
def call(number)
"foo #{number}"
end
def call
"foo 42"
end
end
class Bar
def call(number)
"bar #{number}"
end
end
foo = Foo.new
bar = Bar.new
Benchmark.ips do |x|
x.report("method overloading") { foo.call(23) }
x.report("method") { bar.call(23) }
x.compare!
end
# Warming up --------------------------------------
# method overloading 126.264k i/100ms
# method 255.798k i/100ms
# Calculating -------------------------------------
# method overloading 1.711M (± 8.2%) i/s - 8.586M in 5.057014s
# method 5.697M (± 3.3%) i/s - 28.649M in 5.035081s
# Comparison:
# method: 5696723.0 i/s
# method overloading: 1711428.7 i/s - 3.33x slower |
This comment has been minimized.
This comment has been minimized.
If anyone wants to continue to improve this, https://github.com/dblock/ruby-overload |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
Using a string as😁
method_id
would make it slightly fasteretc.