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.