Last active
July 13, 2023 20:06
-
-
Save alessandro-fazzi/cf22b3a0e66766bbcc5149e8a4494310 to your computer and use it in GitHub Desktop.
[study] Ruby method overloading in 2023
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
source 'https://rubygems.org' | |
# gem "rails" | |
gem 'benchmark-ips' | |
gem 'rubocop' | |
gem 'stackprof' | |
gem 'stackprof-webnav' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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