[notice]
If you use this sort of technique, you may want to read on.
node = nodes.first rescue return
[/notice]
[important]
Nov 2012 Update:
Though this post was about the performance cost of using a ‘rescue’ statement, there is a more insidious problem with the overall impact of such syntax. The pros and cons of using a rescue are well laid out in Avdi’s free RubyTapas: Inline Rescue
[/important]
Code like this:
unless nodes.nil? nodes.first else return end
Can be written using the seemingly more elegant approach with this ruby trick:
node = nodes.first rescue return
But then, that got me to thinking… In many languages I have used in the past (e.g., Java and C++), Exception handling is an expensive endeavor.
So, though the rescue solution works, I am thinking I should explore whether there are any pros/cons to allowing a “rescue” to act as logic. So I did just that…
Here are the two methods I benchmarked, one with “if” logic, and one with “rescue” logic:
def without_rescue(nodes) return nil if nodes.nil? node = nodes.first end
def with_rescue(nodes) node = nodes.first rescue return end
Using method_1
, below, I got the following results looping 1 million times:
user system total real W/out rescue 0.520000 0.010000 0.530000 ( 0.551359) With rescue 22.490000 0.940000 23.430000 ( 26.487543)
Yikes. Obviously, rescue is an expensive choice by comparison!
But, if we look at just one or maybe 10 times, the difference is imperceptible.
Conclusion #1 (Normal Usage)
- It doesn’t matter which method you chose to use if the logic is invoked infrequently.
Looking a bit Deeper
But being a curious engineer at heart, there’s more… The above results are based on worst-case, assuming nodes
is always nil. If nodes
is never nil, then the rescue block is never invoked. Yielding this (rather obvious) timing where the rescue technique (with less code) is faster:
user system total real W/out rescue 0.590000 0.000000 0.590000 ( 0.601803) With rescue 0.460000 0.000000 0.460000 ( 0.461810)
However, what if nodes were only nil some percentage of the time? What does the shape of the performance curve look like? Linear? Exponential? Geometric progression? Well, it turns out that the response (see method_2
, below) is linear (R2= 0.99668):
Conclusion #2 (Large Data Set):
In this example use of over a million tests, the decision on whether you should use “rescue” as logic boils down to this:
- If the condition is truly rare (like a real exception), then you can use rescue.
- If the condition is going to occur 5% or more, then do not use rescue technique!
In general, it would seem that there is considerable cost to using rescue as pseudo logic over large data sets. Caveat emptor!
Sample Code:
My benchmarking code looked like this:
require 'benchmark' include Benchmark def without_rescue(nodes) return nil if nodes.nil? node = nodes.first end def with_rescue(nodes) node = nodes.first rescue return end TEST_COUNT = 1000000 def method_1 [nil, [1,2,3]].each do |nodes| puts "nodes = #{nodes.inspect}" GC.start bm(12) do |test| test.report("W/out rescue") do TEST_COUNT.times do |n| without_rescue(nodes) end end test.report("With rescue") do TEST_COUNT.times do |n| with_rescue(nodes) end end end end end def method_2 GC.start bm(18) do |test| nil_nodes = nil real_nodes = nodes = [1,2,3] likely_pct = 0 10.times do |p| likely_pct += 10 test.report("#{likely_pct}% W/out rescue") do TEST_COUNT.times do |n| nodes = rand(100) > likely_pct ? real_nodes : nil_nodes without_rescue(nodes) end end test.report("#{likely_pct}% With rescue") do TEST_COUNT.times do |n| nodes = rand(100) > likely_pct ? real_nodes : nil_nodes with_rescue(nodes) end end end end end method_1 method_2
Sample Output
user system total real W/out rescue 0.520000 0.010000 0.530000 ( 0.551359) With rescue 22.490000 0.940000 23.430000 ( 26.487543) nodes = [1, 2, 3] user system total real W/out rescue 0.590000 0.000000 0.590000 ( 0.601803) With rescue 0.460000 0.000000 0.460000 ( 0.461810) user system total real 10% W/out rescue 1.020000 0.000000 1.020000 ( 1.087103) 10% With rescue 3.320000 0.120000 3.440000 ( 3.825074) 20% W/out rescue 1.020000 0.000000 1.020000 ( 1.036359) 20% With rescue 5.550000 0.200000 5.750000 ( 6.158173) 30% W/out rescue 1.020000 0.010000 1.030000 ( 1.105184) 30% With rescue 7.800000 0.300000 8.100000 ( 8.827783) 40% W/out rescue 1.030000 0.010000 1.040000 ( 1.090960) 40% With rescue 10.020000 0.400000 10.420000 ( 11.028588) 50% W/out rescue 1.020000 0.000000 1.020000 ( 1.138765) 50% With rescue 12.210000 0.510000 12.720000 ( 14.080979) 60% W/out rescue 1.020000 0.000000 1.020000 ( 1.051054) 60% With rescue 14.260000 0.590000 14.850000 ( 15.838733) 70% W/out rescue 1.020000 0.000000 1.020000 ( 1.066648) 70% With rescue 16.510000 0.690000 17.200000 ( 18.229777) 80% W/out rescue 0.990000 0.010000 1.000000 ( 1.099977) 80% With rescue 18.830000 0.800000 19.630000 ( 21.634664) 90% W/out rescue 0.980000 0.000000 0.980000 ( 1.325569) 90% With rescue 21.150000 0.910000 22.060000 ( 25.112102) 100% W/out rescue 0.950000 0.000000 0.950000 ( 0.963324) 100% With rescue 22.830000 0.940000 23.770000 ( 25.327054)
You miss the main reason why “rescue nil” has been sternly frowned upon for years: If “node” isn’t set or doesn’t have a #first, or if #first throws a real exception, you never get to see it.
Software that masks error situations instead of ensuring they get handled and/or reported is the number 1 technical bane of our industry.
Yes, I can see that point of view and certainly agree that masking errors is not usually the prudent course of action.
In the particular instances where I have seen it used by my compatriots (and have used it myself), it is typically to accommodate data that (legitimately) may or may not be present. Therefore, it is not so much an error, but rather just a valid condition where something might truly be nil/empty. For example, in UI code where we will display something if it exists, I used to us “if” logic, but have since begun to copy folks who use the more compact “rescue ”” technique.
And the purist in me says rescue implies an exceptional condition. However, in the cases where we use this technique, it actually must *not* be an exceptional condition — a contradiction. Hence I referred to this as a “ruby trick,” as I see it is a cute way to save a line or two of code.
So what does the community feel about this sort of thing?
Nonetheless, I will take your point to heart and be on the lookout for ill-conceived usages in our code. And I will reconsider the usage even in our current “benign” conditions, as it does offend the purist in me, even though it makes the pragmatist in me happy.
Nil or empty may not be exceptional conditions; but using rescue steers real exceptional conditions down the same code path. The community generally accepts that this is worse than just a smell; it’s outright wrong.
If you want to conditionally display something, use or or and/or:
or
The other gimmick is #try, implemented in Rails, which avoids the duplication of list.first:
Whatever you choose, please make sure you never mask a potential real exception. A lot of real systems have real bugs as a result of this lazy practise.
Thanks for the tips!