Benchmarking Ruby exception handling
Which is better, handling an exception or explicitly checking to see whether or not your code is going to break? The answer is "it depends". On the one hand exception handling allows us to write more legible code (often summed up by the saying "it's easier to ask forgiveness than permission"). On the other, handling an exception is often a costly operation; it can be faster to "look before you leap".
But just how costly is exception handling? Happily it's very easy to find out. I was working away with my pair recently when this very question came up. We were calculating a ratio between two numbers, but wanted to modify the calculation so that if the denominator was 0, we'd use 0.5 instead. Our function looked something like this:
def calculate_ratio(apples, oranges)
apples / oranges
rescue ZeroDivisionError
apples / 0.5
end
It's succinct and, I think, very readable. We call this function a lot, and in a large percentage of those calls oranges will be 0. Just how expensive is that? There's only one way to find out. Test it!
The Benchmark
module contains a few nifty functions for testing this kind of scenario. I wrote two functions, ask_forgiveness
and ask_permission
and ran each of them a few million times with the bm()
function.
require "benchmark"
include Benchmark
LOOP_COUNT = 1000000
def ask_forgiveness(a)
1 / a
rescue ZeroDivisionError
1 / 0.5
end
def ask_permission(a)
denominator = (a == 0) ? 0.5 : a
1 / denominator
end
bm(5) do |test|
test.report("Forgiveness:") do
LOOP_COUNT.times do |i|
ask_forgiveness(i % 2)
end
end
test.report(" Permission:") do
LOOP_COUNT.times do |i|
ask_permission(i % 2)
end
end
end
We assumed that 50% of our function calls would result in a ZeroDivisionError, which is why we're passing i % 2
into our two functions (it alternates between 0 and 1).
And the results?
user system total real
Forgiveness: 6.780000 0.360000 7.140000 ( 7.172876)
Permission: 1.080000 0.010000 1.090000 ( 1.090219)
A clear win (in this case) for checking ahead of time. It was worth benchmarking as I'd normally go with the more legible exception handling approach every time.
Exception handling is at it's best when exceptions really are the exception to the rule...