Skip to content

Instantly share code, notes, and snippets.

@alessandro-fazzi
Last active July 13, 2023 20:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alessandro-fazzi/cf22b3a0e66766bbcc5149e8a4494310 to your computer and use it in GitHub Desktop.
Save alessandro-fazzi/cf22b3a0e66766bbcc5149e8a4494310 to your computer and use it in GitHub Desktop.
[study] Ruby method overloading in 2023
# frozen_string_literal: true
source 'https://rubygems.org'
# gem "rails"
gem 'benchmark-ips'
gem 'rubocop'
gem 'stackprof'
gem 'stackprof-webnav'
# ensure to `bundle install` before running the script.
# Trying to reimplement method overloading with little new ideas in 2023.
# See:
# https://github.com/dblock/ruby-overload/tree/master
# https://gist.github.com/jodosha/e3097ed693e9b7c255b658ac39c2e403
# NOTE: some of these gems are not working when loaded through bundler/inline
require 'bundler/setup'
require 'stackprof'
require 'benchmark/ips'
# rubocop:disable Style/Documentation, Metrics/AbcSize, Metrics/MethodLength
module Overduke
def self.included(base)
base.extend ClassMethods
base.include Proxy
end
module Proxy
end
module ClassMethods
def overloaded(method_name)
m = instance_method(method_name)
@__overloaded_methods ||= Class.new(m.owner).new
parameters = m.parameters
parameters_args = parameters.filter_map { _1.last if %i[opt req rest].include? _1[0] }
parameters_kwargs = parameters.filter_map { _1.last if %i[key keyreq keyrest].include? _1[0] }
sig_from_params = :"#{parameters_args.size}_#{parameters_kwargs}_#{method_name}"
@__overloaded_methods.define_singleton_method(sig_from_params, m)
remove_method(method_name)
# Early returning if the proxy method with this name is already defined: we need only one
# proxy method per overloaded method name
return if Proxy.method_defined?(method_name)
Proxy.define_method(method_name) do |*args, **kwargs|
sig_from_arguments = :"#{args.size}_#{kwargs.keys}_#{method_name}"
begin
self.class.instance_variable_get(:"@__overloaded_methods").send(sig_from_arguments, *args, **kwargs)
rescue NoMethodError
"Unknown method signature for #{self.class}##{__method__}"
end
end
end
end
end
# BENCHMARKING UTILITIES
class Bar
def foo(bar) = bar
end
class BazParent
def foo(bar) = bar
end
class Baz < BazParent; end
module SausageMod
def foo(bar) = bar
end
class Sausage
include SausageMod
end
class Rusty
def foo(bar = nil, baz: nil)
return "#{bar} - #{baz}" if bar && baz
return bar if bar && !baz
return baz if baz && !bar
end
end
bar = Bar.new
baz = Baz.new
sausage = Sausage.new
rusty = Rusty.new
# END BENCHMARKING UTILITIES
class Foo
include Overduke
overloaded def foo = 'No arguments: nothing to elaborate'
overloaded def foo(bar) = bar
overloaded def doing_work
repetitions = 1
"Generating string #{repetitions} time."
end
overloaded def foo(bar, baz:) = [bar, baz]
overloaded def foo(bar, sausage:) = [bar, sausage]
def fast(bar) = bar
overloaded def doing_work(repetitions: 1000)
repetitions.times do
"Generating string #{repetitions} times."
end
end
def raw_doing_work(repetitions: 1)
return "Generating string #{repetitions} time" if repetitions == 1
repetitions.times do
"Generating string #{repetitions} times."
end
end
end
foo = Foo.new
# binding.irb
p foo.foo
p foo.foo('bar', 'baz', pee: 'pee', l: 'l')
p foo.foo('bar')
p foo.foo('bar', baz: 'baz')
p foo.foo('bar', sausage: 'sausage')
# STDOUT
# ❯ ruby overduke.rb
# "No arguments: nothing to elaborate"
# "Unknown method signature for Foo#foo"
# "bar"
# "bar - baz"
# "bar - sausage"
# StackProf.run(mode: :cpu, out: 'profiling/stackprof-cpu-myapp.dump') do
# 1_000_000.times do
# foo.foo(do_some_real_work: true)
# end
# end
# Run
# bundle exec stackprof-webnav -d profiling/
# to explore stackprof's data
# Benchmark.ips do |x|
# x.report('raw') { bar.foo('bar') }
# x.report('overloaded') { foo.foo('bar') }
# x.report('overloaded class non overloaded method') { foo.fast('bar') }
# x.report('class inheritance') { baz.foo('bar') }
# x.report('module mixin') { sausage.foo('bar') }
# x.report('rusty') { rusty.foo('bar') }
# x.report('doing some work overloaded') { foo.doing_work(repetitions: 1000) }
# x.report('doing some work raw') { foo.raw_doing_work(repetitions: 1000) }
# x.compare!
# end
# Benchmark.ips do |x|
# x.report('doing some work overloaded') { foo.doing_work(repetitions: 1000) }
# x.report('doing some work raw') { foo.raw_doing_work(repetitions: 1000) }
# x.compare!
# end
# rubocop:enable Style/Documentation, Metrics/AbcSize, Metrics/MethodLength
# Warming up --------------------------------------
# raw 1.291M i/100ms
# overloaded 155.343k i/100ms
# overloaded class non overloaded method
# 1.310M i/100ms
# class inheritance 1.308M i/100ms
# module mixin 1.308M i/100ms
# rusty 786.944k i/100ms
# doing some work overloaded
# 893.000 i/100ms
# doing some work raw 890.000 i/100ms
# Calculating -------------------------------------
# raw 12.952M (± 0.8%) i/s - 65.863M in 5.085481s
# overloaded 1.531M (± 1.5%) i/s - 7.767M in 5.075464s
# overloaded class non overloaded method
# 12.909M (± 0.6%) i/s - 65.475M in 5.072200s
# class inheritance 12.845M (± 0.4%) i/s - 65.386M in 5.090590s
# module mixin 12.940M (± 0.3%) i/s - 65.379M in 5.052466s
# rusty 7.886M (± 0.3%) i/s - 40.134M in 5.089048s
# doing some work overloaded
# 8.927k (± 0.3%) i/s - 44.650k in 5.001468s
# doing some work raw 9.034k (± 0.2%) i/s - 45.390k in 5.024590s
# Comparison:
# raw: 12952026.1 i/s
# module mixin: 12940194.3 i/s - same-ish: difference falls within error
# overloaded class non overloaded method: 12909162.3 i/s - same-ish: difference falls within error
# class inheritance: 12844809.8 i/s - same-ish: difference falls within error
# rusty: 7886428.5 i/s - 1.64x slower
# overloaded: 1530716.9 i/s - 8.46x slower
# doing some work raw: 9033.6 i/s - 1433.76x slower
# doing some work overloaded: 8927.5 i/s - 1450.81x slower
# Notable observations about benchmarks:
# - `rusty` with just 3 conditionals inside is 1.6x slower than "direct" calls. This demonstrates
# that measurements method calls on empty or almost empty method are just for science
# - `doing some work overloaded` almost fast as `doing some work raw`.
# Actually other benchmarks are only measuring method call speed, but in the real world
# most execution time is expanded in computation, not in the call per se.
# We must also keep in mind that method overloading allows us to skip conditional branches
# in out definitions, recovering a bit of performance loss.
#
# Just as memorandum here it is a scoped benchmarking:
#
# Warming up --------------------------------------
# doing some work overloaded
# 894.000 i/100ms
# doing some work raw 905.000 i/100ms
# Calculating -------------------------------------
# doing some work overloaded
# 8.926k (± 0.5%) i/s - 44.700k in 5.008099s
# doing some work raw 9.057k (± 0.3%) i/s - 46.155k in 5.096332s
# Comparison:
# doing some work raw: 9056.6 i/s
# doing some work overloaded: 8925.7 i/s - 1.01x slower
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment