Avoid Using Metaprogramming
Ruby is sexy, Ruby is cool and its metaprogramming potential offers some really cook features. However you might not realize that your cleverness is slowing down your code.
Today I was working on cleaning up merb_helper a Merb plugin that brings a lot of the stuff Rails developers are used to. In Merb we aim for speed and try to avoid magic.
merb_plugin didn’t receive a lot of love from the main contributors but few features were added by different contributors and the code became hard to maintain.
Looking at the code I quickly found this bad boy:
(Old Merb Time DSL using metaprogramming)
module MetaTimeDSL
{:second => 1,
:minute => 60,
:hour => 3600,
:day => [24,:hours],
:week => [7,:days],
:month => [30,:days],
:year => [364.25, :days]}.each do |meth, amount|
define_method "m_#{meth}" do
amount = amount.is_a?(Array) ? amount[0].send(amount[1]) : amount
self * amount
end
alias_method "m_#{meth}s".intern, "m_#{meth}"
end
end
Numeric.send :include, MetaTimeDSL
The above code looks awful to me and I decided to rewrite it a way I thought would be more efficient:
module TimeDSL
def second
self * 1
end
alias_method :seconds, :second
def minute
self * 60
end
alias_method :minutes, :minute
def hour
self * 3600
end
alias_method :hours, :hour
def day
self * 86400
end
alias_method :days, :day
def week
self * 604800
end
alias_method :weeks, :week
def month
self * 2592000
end
alias_method :months, :month
def year
self * 31471200
end
alias_method :years, :year
end
Numeric.send :include, TimeDSL
To make sure I was right, I run the following benchmarks:
require 'benchmark'
TIMES = (ARGV[0] || 100_000).to_i
Benchmark.bmbm do |x|
x.report("metaprogramming 360.seconds") do
TIMES.times do
360.m_seconds
end
end
x.report("no metaprogramming 360.hours") do
TIMES.times do
360.seconds
end
end
x.report("metaprogramming 360.minutes") do
TIMES.times do
360.m_minutes
end
end
x.report("no metaprogramming 360.minutes") do
TIMES.times do
360.minutes
end
end
x.report("metaprogramming 360.hours") do
TIMES.times do
360.m_hours
end
end
x.report("no metaprogramming 360.hours") do
TIMES.times do
360.hours
end
end
x.report("metaprogramming 360.days") do
TIMES.times do
360.m_days
end
end
x.report("no metaprogramming 360.days") do
TIMES.times do
360.days
end
end
x.report("metaprogramming 360.weeks") do
TIMES.times do
360.m_weeks
end
end
x.report("no metaprogramming 360.weeks") do
TIMES.times do
360.weeks
end
end
x.report("metaprogramming 18.months") do
TIMES.times do
18.m_months
end
end
x.report("no metaprogramming 18.months") do
TIMES.times do
18.months
end
end
x.report("metaprogramming 7.years") do
TIMES.times do
7.m_years
end
end
x.report("no metaprogramming 7.years") do
TIMES.times do
7.years
end
end
end
Rehearsal ------------------------------------------------------------------
metaprogramming 360.seconds 0.130000 0.000000 0.130000 ( 0.133164)
no metaprogramming 360.hours 0.050000 0.000000 0.050000 ( 0.042655)
metaprogramming 360.minutes 0.130000 0.000000 0.130000 ( 0.133327)
no metaprogramming 360.minutes 0.040000 0.000000 0.040000 ( 0.042401)
metaprogramming 360.hours 0.140000 0.000000 0.140000 ( 0.134312)
no metaprogramming 360.hours 0.040000 0.000000 0.040000 ( 0.043125)
metaprogramming 360.days 0.130000 0.000000 0.130000 ( 0.134949)
no metaprogramming 360.days 0.050000 0.000000 0.050000 ( 0.043745)
metaprogramming 360.weeks 0.130000 0.000000 0.130000 ( 0.135581)
no metaprogramming 360.weeks 0.050000 0.000000 0.050000 ( 0.043544)
metaprogramming 18.months 0.130000 0.000000 0.130000 ( 0.135234)
no metaprogramming 18.months 0.050000 0.000000 0.050000 ( 0.044354)
metaprogramming 7.years 0.140000 0.000000 0.140000 ( 0.144062)
no metaprogramming 7.years 0.050000 0.000000 0.050000 ( 0.044392)
--------------------------------------------------------- total: 1.260000sec
user system total real
metaprogramming 360.seconds 0.130000 0.000000 0.130000 ( 0.132567)
no metaprogramming 360.hours 0.040000 0.000000 0.040000 ( 0.042777)
metaprogramming 360.minutes 0.140000 0.000000 0.140000 ( 0.132554)
no metaprogramming 360.minutes 0.040000 0.000000 0.040000 ( 0.043193)
metaprogramming 360.hours 0.130000 0.000000 0.130000 ( 0.133027)
no metaprogramming 360.hours 0.050000 0.000000 0.050000 ( 0.042613)
metaprogramming 360.days 0.130000 0.000000 0.130000 ( 0.138637)
no metaprogramming 360.days 0.050000 0.000000 0.050000 ( 0.043213)
metaprogramming 360.weeks 0.130000 0.000000 0.130000 ( 0.134049)
no metaprogramming 360.weeks 0.040000 0.000000 0.040000 ( 0.043713)
metaprogramming 18.months 0.140000 0.000000 0.140000 ( 0.134941)
no metaprogramming 18.months 0.040000 0.000000 0.040000 ( 0.043980)
metaprogramming 7.years 0.150000 0.000000 0.150000 ( 0.143389)
no metaprogramming 7.years 0.040000 0.000000 0.040000 ( 0.044585)
0.136591)
The metaprogramming version of the same implementation is almost 3 times slower!
Moral of the story: be careful when using metaprogramming, you might end up slowing down your code considerably.