Home > Uncategorized > A short ruby diversion: cost of flow control under Ruby

A short ruby diversion: cost of flow control under Ruby

Tags:

May 3, 2011 4 Comments »

A couple days ago I decided to finally get back to working on threach to try to deal with problems it had — essentially, it didn’t deal well with non-local exits due to calls to break or even something simple like a NoMethodError.

[BTW, I think I managed it. As near as I can tell, threach version 0.4 won't deadlock anymore]

Along the way, while trying to figure out how threads affect the behavior of different non-local exits, I noticed that in some cases there was still work being done by one or more threads long after there was an exception raised.

I re-discovered something that a lot of people already know: raise/rescue under MRI is slow, and under JRuby can be unbearably slow. How slow?

Let’s look at four simple blocks that exercise four different block exit strategies: break, catch and throw, raise with the normal single (or zero) arguments, as well as the three-argument version of raise.

Simple breakCatch/Throw
range.each do |i|      
  break          
end              
      
    
catch(:benchmarking) do  
 range.each do |i|      
   throw(:benchmarking) 
 end                    
end
      
    
Raise (1 arg)Raise (3 args)
 begin                  
   range.each do |i|    
     raise StandardError
   end                  
 rescue                 
  # do nothing                
 end                          
     
    
begin                  
  range.each do |i|
    raise StandardError, :hi, nil
  end
rescue 
 # do nothing
end
      
    

In each case, we immediately exit the block without doing any work; the idea is to measure how long it takes to break out for each case.

So....let's run them each 100K times and see what happens, shall we? Times are in seconds, averaged over two runs.

Ruby 1.8Ruby 1.9JRubyJRuby --1.9
break 0.120.070.29 0.21
catch/throw 0.350.280.64 0.48
raise (1 arg)1.782.1026.6022.06
raise (3 arg)1.852.130.45 0.45

The first thing to note is that this is 100K iterations. Three of the strategies are fast enough that you'd have to work really, really hard to notice them. In terms of speed, raise (3 args), catch/throw, and break are fast enough that you shouldn't bother worrying about them (although you should choose the method that makes your code easy to understand).

The second things to note is Holy Camoli! JRuby is slow there!

This Jira ticket tells the tale: The creation of the backtrace is very, very expensive for JRuby. That nil at the end of the raise (3 args) call suppresses the creation of that backtrace, so the speed is fine.

Three things worth saying here:

  • If you're using raise/rescue for flow control, you're already doing it wrong. Reserve exceptions for, well, exceptional conditions that are only going to be raised once or twice, not all the time.
  • If you're writing code that, for some ungodly reason, is planning on raising a crapload of exceptions, use the three-arg version. I'm looking at you, gem authors.
  • If you're writing your code without worrying about how it will work under multiple threads, well, please don't do that. Everyone has multi-core systems these days, and it's silly to not be able to use them. Plus, counting on Matz to never move to a VM with real threads is a big gamble.

Tags: ruby

Comments:4

Leave my own
  1. Jonathan Rochkind
    May 4, 2011 at 7:25 pm

    For flow control in ruby, there’s actually a throw/catch architecture, which is an entirely different beast from raise/rescue. Nobody hardly ever uses them, throw/catch, I never see em, never used em myself either.

    Note: raise/rescue DO correspond to JAVA’s throw/catch. ruby’s throw/catch is something different: It can only be used in ‘static scoped’ situations, basically where the catch is in a static code block that’s a parent of the throw. But if people are using raise/rescue for ‘flow control’ scenarios in places where throw/catch would work…. would be interesting to benchmark the performance of throw/catch. throw/catch at least is indeed actually intended for flow control.

    Maybe nobody uses em cause they smell suspiciosuly like the dreaded ‘goto’, but that’s essentially what you’re doing with raise/rescue if you’re using em for flow control too, and apparently that doesn’t stop some people? Very curious what code you saw that was using raise/rescue like this, it’s certainly not a recommended thing to do by anyone (I don’t think?).

  2. Jonathan Rochkind
    May 4, 2011 at 7:27 pm

    PS: Am I the only one that never uses those raise syntactic sugar shortcuts? I always actually create the Exception object myself:

    raise StandardError.new

    “raise StandardError” does the same thing, it’s just a shortcut. And:

    raise StandardError, “message” ==== raise StandardError.new(“message”)

    I don’t know the way to avoid backtrace generation when throwing an actually explicitly created Exception object, but there probably is one.

  3. Jonathan Rochkind
    May 4, 2011 at 7:29 pm

    And briefly looking up the documentation on throw/catch, I’m wrong about the catch having to be statically scoped in a block above the ‘throw’ (the page I found in the online old ruby book actually specifically tells you this isn’t the case even though you might think it is, heh). But I’m still confused about where throw/catch can actually be used. It’s like the least used ruby language feature ever. But if lots of people are using raise/rescue for flow control, maybe throw/catch ought to be marketted better.

  4. Jonathan Rochkind
    June 18, 2011 at 10:37 am

    Another blog figures out the same thing, posted on reddit. You beat them to it! http://www.coffeepowered.net/2011/06/17/jruby-performance-exceptions-are-not-flow-control/

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>