Wednesday, January 9, 2008

Code Efficiency in Ruby

I got interested in the qualify of Ruby code in Rails when I noticed what appears to me to be a useless method in the ActiveRecord code. Specifically, ActiveRecord::Base.save is a public method which calls the private method ActiveRecord::Base#create_or_update. That doesn't make a lot of sense to me, because it could be replaced by making 'create_or_update' public and then aliasing 'save' to it.

So I decided to check to see what the superfluously method call costs.

I wrote a test which performed a simple task [incrementing an instance variable by a random number between 1 and 10] using five (5) different ways of accessing the instance variable and invoking the action.

The class definition is at the bottom of this post.

I then ran these methods 10,000,000 times using four different ways of invoking the methods:

  • directly calling the methods - e.g. foo.inc_instance_variable()

  • invoking via the method's 'call' attribute - e.g. foo.inc_instance_variable.call()

  • invoking via the method via 'send' - e.g. foo.send('inc_instance_variable')

  • invoking via 'eval'ing the string - e.g. eval 'foo.inc_instance_variable'

The Precent Results are simply the run time divided by the minimum run time for all tests converted to a percent increase.

Here's the summary:

  • Invoking via an Alias doesn't cost anything

  • unnecessarily accessing an instance Variable via an accessor slows down about 20%

  • The unnecessary method/function call slows down by about 25%

  • Combining the unnecessary call with accessor access slows down about 45% - so the effect is linear

  • Invoking by the 'call' method slows it down by about 13%

  • Using 'send' slows down about an additional 45%

  • Using eval slows the process down by something on the order of 400%, but the effect is not linear, so 'eval' must be doing some additional mucking about.

So, what's the point? None, if you're satisfied with glacial execution speeds.

On the other hand, it's something you should know if you are writing critical code and have to make choices about how to implement it.

As usual, your mileage may vary. The full program code is at http://www.clove.com/downloads/method-call-timing-tests.rb.

Here are the detailed Percentage Results

Percent Results for direct method of invocation
foo.inc_var_as_instance 0.99
foo.inc_var_as_instance_alias 0.00
foo.inc_var_as_method 19.89
foo.inc_var_as_func_and_instance 25.28
foo.inc_var_as_func_and_method 45.17

Percent Results for call method of invocation
foo.inc_var_as_instance 14.06
foo.inc_var_as_instance_alias 12.93
foo.inc_var_as_method 32.53
foo.inc_var_as_func_and_instance 42.19
foo.inc_var_as_func_and_method 59.52

Percent Results for send method of invocation
foo.inc_var_as_instance 42.05
foo.inc_var_as_instance_alias 46.02
foo.inc_var_as_method 60.65
foo.inc_var_as_func_and_instance 75.57
foo.inc_var_as_func_and_method 94.03

Percent Results for eval method of invocation
foo.inc_var_as_instance 393.89
foo.inc_var_as_instance_alias 410.94
foo.inc_var_as_method 420.03
foo.inc_var_as_func_and_instance 458.10
foo.inc_var_as_func_and_method 577.84

Here are the raw timing results:

Result using Direct Calls
foo.inc_var_as_instance 7.070000 0.040000 7.110000 ( 7.306318)
foo.inc_var_as_instance_alias 7.000000 0.040000 7.040000 ( 7.155283)
foo.inc_var_as_method 8.390000 0.050000 8.440000 ( 8.585970)
foo.inc_var_as_func_and_instance 8.780000 0.040000 8.820000 ( 8.950350)
foo.inc_var_as_func_and_method 10.160000 0.060000 10.220000 ( 10.378127)

Result using .call
foo.inc_var_as_instance 7.990000 0.040000 8.030000 ( 8.204646)
foo.inc_var_as_instance_alias 7.910000 0.040000 7.950000 ( 8.061320)
foo.inc_var_as_method 9.280000 0.050000 9.330000 ( 9.502992)
foo.inc_var_as_func_and_instance 9.940000 0.070000 10.010000 ( 10.249010)
foo.inc_var_as_func_and_method 11.170000 0.060000 11.230000 ( 11.439927)

Result using 'foo.send '
foo.inc_var_as_instance 9.950000 0.050000 10.000000 ( 10.220120)
foo.inc_var_as_instance_alias 10.220000 0.060000 10.280000 ( 10.462063)
foo.inc_var_as_method 11.250000 0.060000 11.310000 ( 11.514329)
foo.inc_var_as_func_and_instance 12.290000 0.070000 12.360000 ( 12.583919)
foo.inc_var_as_func_and_method 13.590000 0.070000 13.660000 ( 13.891333)

Result using 'eval '
foo.inc_var_as_instance 34.550000 0.220000 34.770000 ( 35.387702)
foo.inc_var_as_instance_alias 35.740000 0.230000 35.970000 ( 36.670451)
foo.inc_var_as_method 36.390000 0.220000 36.610000 ( 37.296297)
foo.inc_var_as_func_and_instance 39.050000 0.240000 39.290000 ( 40.033456)
foo.inc_var_as_func_and_method 47.420000 0.300000 47.720000 ( 48.638035)

Here's the class definition:

class Foo
attr_accessor :var

def initialize
@var = 0
end

# access the instance variable directly
def inc_var_as_instance
@var = @var + 1 + rand(10)
end
# access instance variable directly, but us an alias
alias_method :inc_var_as_instance_alias, :inc_var_as_instance

# access instance via accessor method, even though inside class instance
def inc_var_as_method
self.var = self.var + 1 + rand(10)
end

public
# add an additional method call to accessing via direct access to instance variable
def inc_var_as_func_and_instance
inc_var_as_instance
end

# add an additional method call to accessing via accessor
def inc_var_as_func_and_method
inc_var_as_method
end
end

No comments: