Exceptions kill your apps performance

By Radek Bułat, 8 May 2015

While programming in Ruby on a daily basis, we probably don’t care too much about performance. We just write a code and if it works relatively fast, it does not raise our doubts. It is worth to have in mind that Ruby, just like any other language, consists of structures, which have a diversified performance characteristics. One of the most tricky structures are the exceptions. If you would like to stop reading this article here, I would like you to know one thing: exceptions are slow - use them in exceptional situations only.

raise

How slow are the exceptions? Let’s check it with a simple benchmark:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require "benchmark"

def simple
  1 + 1
end

def simple_with_exception
  raise "error"
rescue
  1 + 1
end

Benchmark.bmbm do |x|
  N = 1_000_000
  x.report("simple") { N.times { simple } }
  x.report("simple_with_exception") { N.times { simple_with_exception } }
end
1
2
3
4
5
6
7
8
9
10
$ ruby -v && ruby bm1.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Rehearsal ---------------------------------------------------------
simple                  0.090000   0.000000   0.090000 (  0.086732)
simple_with_exception   1.750000   0.030000   1.780000 (  1.796754)
------------------------------------------------ total: 1.870000sec

                            user     system      total        real
simple                  0.090000   0.000000   0.090000 (  0.093416)
simple_with_exception   1.730000   0.030000   1.760000 (  1.775019)

The method simple_with_exception is about 20 times slower on my machine than the method simple. This simple example shows how big the cost of raising and intercepting an exception is. However, the cost can be even higher. Every exception bears an information about callstack (Exception#backtrace). We can assume then, that the bigger the callstack, the bigger the cost of raising an exception. So let’s check out our theory!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
require "benchmark"

def bigger_callstack(n, &block)
  n == 0 ? yield : bigger_callstack(n-1, &block)
end

def simple
  1 + 1
end

def simple_with_exception
  raise "error"
rescue
  1 + 1
end

size = ARGV[0].to_i

bigger_callstack(size) do
  puts "Stack size: #{caller.size}"

  Benchmark.bmbm do |x|
    N = 1_000_000
    x.report("simple") { N.times { simple } }
    x.report("simple_with_exception") { N.times { simple_with_exception } }
  end
end
1
2
3
4
5
6
7
8
9
10
11
$ ruby -v && ruby bm2.rb 100
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Stack size: 102
Rehearsal ---------------------------------------------------------
simple                  0.090000   0.000000   0.090000 (  0.085693)
simple_with_exception   2.470000   0.250000   2.720000 (  2.734295)
------------------------------------------------ total: 2.810000sec

                            user     system      total        real
simple                  0.090000   0.000000   0.090000 (  0.089928)
simple_with_exception   2.480000   0.260000   2.740000 (  2.747601)

For the callstack of 100 (a typical Rails app) the difference has raised to 30 times.

Unfortunately, this is not the end. Some libraries, such as spring, overwrite the Kernel#raise method (you know that this is a method, right?). The above mentioned spring does it to remove entries regarding this library from the callstack. This issue concerns the environments, in which we use spring, so, first and foremost, test and development.

But let’s get back to the point of the issue. We already know that exceptions are slow (and Ruby is not an exception here ;-)). Does it mean that we shouldn’t use exceptions at all? Of course not! However, we should use them in exceptional (sic!) situations, and not to control the running application. My observation is that, unfortunately, exceptions are overused. Let me give a couple of examples of such overuses.

1
collection.total_pages rescue 0

Assuming that the author wants to protect himself from the situation when there is no total_pages method, it would be better to write:

1
collection.respond_to?(:total_pages) collection.total_pages : 0

Another example comes from the paperclip_database gem.

1
2
3
4
5
6
def setup_attachment_class
  instance.class.ancestors.each do |ancestor|
    names_for_ancestor = ancestor.attachment_definitions.keys rescue []
    # ...
  end
end

Definitely, it would be better to write:

1
2
3
4
5
6
7
8
def setup_attachment_class
  instance.class.ancestors.each do |ancestor|
    next unless ancestor.respond_to?(:attachment_definitions)

    names_for_ancestor = ancestor.attachment_definitions.keys
    # ...
  end
end

The second version is a lot faster.

And another example, this time from the money library.

1
2
3
4
5
6
7
8
9
  def initialize(obj, currency = Money.default_currency, bank = Money.default_bank)
    @fractional = obj.fractional
    @currency   = obj.currency
    @bank       = obj.bank
  rescue NoMethodError
    @fractional = as_d(obj)
    @currency   = Currency.wrap(currency)
    @bank       = bank
  end

Fortunately, it has been fixed.

We still have one problem to solve. We would like to know where and when the exceptions are raised in our application. We could try to search the sources of our application by entering „rescue”. But it is not sufficient because this way we won’t find them in external libraries. In order to do this, we will use the TracePoint class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
errors = {}

TracePoint.new(:raise) do |tp|
  key = "#{tp.raised_exception.to_s} #{tp.path}:#{tp.lineno}"

  if !errors.has_key?(key)
    errors[key] = [1, tp.raised_exception.backtrace]
  else
    errors[key][0] += 1
  end
end.enable

at_exit do
  errors.sort_by { |_, (count, _)| count }.each do |error, (count, backtrace)|
    puts "(#{count}) #{error}"
    puts backtrace.map { |line| "    " + line }.join("\n")
    puts
  end
end

I have included this code in the spec/spec_helper.rb file, and then I have run the tests. Results? Enormous amount of exceptions, such as LoadError. Now I only have to correct them and do the pull requests

About the author

Radek Bułat — Undoubtful Ruby Maestro

"Any problem with Ruby? Ask Radosław" — this is a common saying in Ruby coders' environment. In his work and after-hours he explores world of technology, teaches others and solves the most complex coding issues. Occasionally plays squash.

comments powered by Disqus