Created
October 8, 2018 15:38
-
-
Save JoshCheek/960a9a39fec43c164e322c6310eb5718 to your computer and use it in GitHub Desktop.
try/throw vs raise/rescue in Ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
RUBY_ENGINE # => "ruby" | |
RUBY_VERSION # => "2.5.1" | |
RUBY_PLATFORM # => "x86_64-darwin17" | |
# QUESTION | |
# | |
# Say you want to bail from a web controller as soon as some method is called | |
# Eg in Rails, maybe you want a `render!` method, which is the same as render, | |
# but it also returns from the method that called it. You could accomplish this | |
# with either raise/rescue or throw/catch. This is the sort of thing throw/catch | |
# were meant to handle, but most times that situations like this (control flow) | |
# occur, people use exceptions instead (throw/catch is a bit of a forgotten | |
# features). So, is there any performance cost to choosing the one over the other? | |
# | |
# | |
# CONCLUSION | |
# | |
# * throw / catch is fastest | |
# * raise / rescue is 6.2 times slower if you raise a new error each time | |
# * raise / rescue is 1.5x slower if you allocate 1 error and always throw that one instance | |
# * there is no relevant difference between rescuing the class vs a superclass | |
# * raise / rescue is 40x slower if you access the backtrace (before this was | |
# optimized, its overhead + the fact that Twitter was raising an exception | |
# every time they would iterate over an array, were what led to the fail whale | |
# and their whining about Rails scaling) Imagine switching languages because | |
# you think a language is too slow because every time you call .each, you wind | |
# up incurring this overhead. source: it's somewhere in this hilarious talk | |
# https://www.youtube.com/watch?v=LjFM8vw3pbU | |
class Status < StandardError | |
attr_reader :code | |
def initialize(code) | |
@code = code | |
super "Status #@code" | |
end | |
end | |
class Unauthorized < Status | |
def initialize | |
super 401 | |
end | |
end | |
# raise / rescue, don't allocate the error... | |
# there could be other issues with doing this, eg it's probably not threadsafe | |
# I don't think I've ever seen this in practice | |
@err = Unauthorized.new | |
def will_raise1_reuse() will_raise2_reuse end | |
def will_raise2_reuse() raise @err end | |
will_raise1_reuse rescue $!.code # => 401 | |
# raise / rescue, make a new error each time (much much more common) | |
def will_raise1_allocate() will_raise2_allocate end | |
def will_raise2_allocate() raise Unauthorized end | |
will_raise1_allocate rescue $!.code # => 401 | |
# throw / catch | |
def will_throw1() will_throw2 end | |
def will_throw2() throw :status, 401 end | |
catch(:status) { will_throw1 } # => 401 | |
# kk, lets benchmark them using https://github.com/evanphx/benchmark-ips | |
require 'benchmark/ips' | |
Benchmark.ips do |x| | |
x.report "raise, reusing the same error, rescue the class" do | |
begin will_raise1_reuse | |
rescue Unauthorized # Unauthorized implies 401, so we don't need access to the error to know its arg | |
end | |
end | |
x.report "raise, reusing the same error, rescue a superclass" do | |
begin will_raise1_reuse | |
rescue Status => err # rescuing `Status` vs `Object` had no discernable difference | |
err.code # `Status` is general, so we need access to the error to get its code | |
end | |
end | |
x.report "raise, allocating a new error, rescue the class" do | |
begin will_raise1_allocate | |
rescue Unauthorized | |
end | |
end | |
x.report "raise, allocating a new error, rescue the class, access the backtrace" do | |
begin will_raise1_allocate | |
rescue Unauthorized => err | |
err.backtrace # Ruby has an optimization where it won't build | |
end | |
end | |
x.report "throw / catch" do | |
catch(:status) { will_throw1 } | |
end | |
x.compare! | |
end | |
# >> Warming up -------------------------------------- | |
# >> raise, reusing the same error, rescue the class | |
# >> 80.002k i/100ms | |
# >> raise, reusing the same error, rescue a superclass | |
# >> 76.463k i/100ms | |
# >> raise, allocating a new error, rescue the class | |
# >> 25.144k i/100ms | |
# >> raise, allocating a new error, rescue the class, access the backtrace | |
# >> 4.392k i/100ms | |
# >> throw / catch 103.448k i/100ms | |
# >> Calculating ------------------------------------- | |
# >> raise, reusing the same error, rescue the class | |
# >> 1.277M (± 8.0%) i/s - 6.400M in 5.044363s | |
# >> raise, reusing the same error, rescue a superclass | |
# >> 1.167M (± 9.6%) i/s - 5.811M in 5.030423s | |
# >> raise, allocating a new error, rescue the class | |
# >> 297.901k (± 6.4%) i/s - 1.483M in 5.001602s | |
# >> raise, allocating a new error, rescue the class, access the backtrace | |
# >> 46.733k (± 7.0%) i/s - 232.776k in 5.003789s | |
# >> throw / catch 1.838M (± 8.8%) i/s - 9.207M in 5.050553s | |
# >> | |
# >> Comparison: | |
# >> throw / catch: 1837975.2 i/s | |
# >> raise, reusing the same error, rescue the class: 1277467.8 i/s - 1.44x slower | |
# >> raise, reusing the same error, rescue a superclass: 1166865.2 i/s - 1.58x slower | |
# >> raise, allocating a new error, rescue the class: 297900.6 i/s - 6.17x slower | |
# >> raise, allocating a new error, rescue the class, access the backtrace: 46733.2 i/s - 39.33x slower |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment