Performance Benchmark on Ruby's Dynamic Method
As a dynamic programming language, Ruby parses and compiles your code at runtime. This gives you the ability to call methods dynamically, define methods at runtime or even handle function calls that doesn’t even exist. But how about the performance speed when using these features?
In this post, I will show you two commonly used methods: define_method and method_missing that are used very often in Ruby world to build methods dynamically, and compare them with the normal less-dynamical approach.
define_method
To define a method on the spot, you can use define_method which is included in Module.
Let’s define a class with only 5 empty instance methods using def keyword approach, and define_method approach separately:
class EntryNormal
def e_0; end
def e_1; end
def e_2; end
def e_3; end
def e_4; end
end
class Entry
5.times do |i|
define_method("e_#{i}"){}
end
end
Then compare them by using the benchmark-ips gem:
require 'benchmark/ips'
Benchmark.ips do |x|
x.report("normal method") do
en = EntryNormal.new
en.e_0
en.e_1
en.e_2
en.e_3
en.e_4
end
x.report("dynamic method") do
e = Entry.new
e.e_0
e.e_1
e.e_2
e.e_3
e.e_4
end
x.compare!
end
On my Macbook Air with Ruby 2.1.3p242, I got this result:
Calculating ------------------------------------- normal method 92993 i/100ms dynamic method 74416 i/100ms ------------------------------------------------- normal method 2502298.7 (±6.2%) i/s - 12461062 in 5.001913s dynamic method 1557294.0 (±5.0%) i/s - 7813680 in 5.031780s Comparison: normal method: 2502298.7 i/s dynamic method: 1557294.0 i/s - 1.61x slower
As you can see, it’s about 1.6 times slower when you use define_method.
From the book Metaprogramming Ruby 2 Page 54, there is another approach to DRY the code using define_method. It looks similar like below:
class Entry
# create n number of methods
def initialize(n)
n.times do |i|
Entry.define_component("e_#{i}")
end
end
def self.define_component(name)
define_method(name) do |x|
x
end
end
end
Basically, it defines methods at the initial stage of an object, and you can determine the behavior of these methods accordingly for different object. This is a classic metaprogramming which is defined as “writing code that writes code”.
Let’s compare this with the normal approach below by using the benchmark-ips gem again:
# normal version
class EntryNormal
def e_0(x); x; end
def e_1(x); x; end
def e_2(x); x; end
def e_3(x); x; end
def e_4(x); x; end
end
Benchmark.ips do |x|
x.report("normal method") do
en = EntryNormal.new
en.e_0(1)
en.e_1(2)
en.e_2(3)
en.e_3(4)
en.e_4(5)
end
x.report("dynamic method") do
e = Entry.new(5)
e.e_0(1)
e.e_1(2)
e.e_2(3)
e.e_3(4)
e.e_4(5)
end
end
The result is shown below:
Calculating ------------------------------------- normal method 90761 i/100ms dynamic method 7226 i/100ms ------------------------------------------------- normal method 2388578.5 (±5.0%) i/s - 11980452 in 5.029755s dynamic method 76539.9 (±2.9%) i/s - 382978 in 5.008152s Comparison: normal method: 2388578.5 i/s dynamic method: 76539.9 i/s - 31.21x slower
The speed is over 30 times slower than normal approach! Now you really need to consider if it is worht to use this approach.
method_missing
The method_missing method is really just a private instance method of BasicObject that every Ruby object inherits. If Ruby couldn’t find the method you call anywhere, it will call method_missing method eventually to find the answer.
Let’s rewrite our Entry class with method_missing:
class Entry
def method_missing(method, arg)
arg
end
end
And benchmark this method missing approach with the normal approach:
class EntryNormal
def e_0(x); x; end
def e_1(x); x; end
def e_2(x); x; end
def e_3(x); x; end
def e_4(x); x; end
end
I get the result as shown below:
Calculating ------------------------------------- normal method 92028 i/100ms method missing 84043 i/100ms ------------------------------------------------- normal method 2401237.7 (±4.4%) i/s - 12055668 in 5.031712s method missing 1940226.1 (±4.3%) i/s - 9748988 in 5.034756s Comparison: normal method: 2401237.7 i/s method missing: 1940226.1 i/s - 1.24x slower
The speed is 1.24 times slower. The speed mainly depends on the complexity of your classes dependencies especially your inheritance hierarchy, because method_missing will call after Ruby finishes the entire method lookup.
In our case, the method_missing approach performs better than define_method. And the obvious fact is that method_missing does not really define any method for you.
However, we should be aware of that method_missing can be confusing and dangerous. For example when you use respond_to? method to check if the method exists before you call the method, it will return false
, because it does not exist.
class Entry
def method_missing(method, arg)
arg
end
end
en = Entry.new
en.respond_to?(:e_0) # returns false, since :e_0 is not defined
en.e_0("x") # but you can still call this 'method' => "x"
To make it work, you need to override respond_to_missing? method:
class Entry
@@my_methods = 5.times.map{|x| "e_#{x}"}
def method_missing(method, arg)
# only handles certain methods.
# in our case, e0 ... e4
if @@my_methods.include?(method.to_s)
arg
else
# will raise 'undefined method'
# exception for other methods
super
end
end
def respond_to_missing?(method, include_all=true)
@@my_methods.include?(method.to_s) || super
end
end
en = Entry.new
en.respond_to?(:e_0) # => true
en.respond_to?(:fool) # => false
As you can see from the above example, when calling respond_to?
again, it will return true for method e_0, but false to other undefined methods.