I've always strongly suspected that exceptions would be slow in ruby compared to other flow control mechanisms. After all - exceptions are a heck of a lot more complicated than a simple "break" or "return." But I've been wrong in my hunches before, so I thought I'd put it to the test.
In the code below I'm using the benchmark-ips gem to compare the relative performance of exiting a loop via exception, break and return. I've seen examples on the web of people doing benchmarks like this with mri 1.9. But I wanted to try it out with mri 2.2.
require 'benchmark/ips'
def exit_via_exception
5.times do
raise RuntimeError
end
rescue
end
def exit_via_break
5.times do
break
end
end
def exit_via_return
5.times do
return
end
end
Benchmark.ips do |x|
x.report("exception") { exit_via_exception }
x.report("break") { exit_via_break }
x.report("return") { exit_via_return }
end
The results are pretty staggering. The function using the exception is less than half as fast as those using break and return.
$ ruby exception_benchmark.rb
Calculating -------------------------------------
exception 50.872k i/100ms
break 125.322k i/100ms
return 124.173k i/100ms
-------------------------------------------------
exception 714.795k (± 2.7%) i/s - 3.612M
break 3.459M (± 3.1%) i/s - 17.294M
return 3.379M (± 3.0%) i/s - 16.888M
This isn't a perfect benchmark
There are a couple of issues that I'm not sure how to compensate for. For example, the exception and break methods have to return. So they're doing more than the method which simply returns. Also I'd be interested to see if rescuing the exception adds to the performance overhead. But not rescuing it causes the benchmark to abort.
Still, exception is so much slower than the other examples that I think the results have meaning even if they're not perfect.
The lesson we learned?
If you're using exceptions as a flow control mechanism. Stop now! Especially if you have a loop consisting of exceptions being raised and caught over and over.
Will this change how I personally use exceptions? Probably not. I can live with a little slowness if the slowness is an exception to the rule. :)
...But what about JRuby and RBx?
Josh Cheek (@josh_cheek on twitter) wrote his own version of this benchmark which is more comprehensive then mine. And he ran it against multiple ruby implementations. You can see his results here. Apparently break is still the winner. :)