In describing their process upgrading from Rails 3.2 to Rails 4.2 at February’s Ruby Lightning Talks T.O., Canadian startup 500px experienced significant drops in performance. To underscore the issue, they shared the following example:
Wait a minute. Rails 4 is slower???
Sure seems that way. This simple test shared by Kevin at 500px sure seems clear: (0..5).each { |i| }
is 4 times faster on Rails 3.2 compared to Rails 4.2. This is a significant drop in performance. Lets find out why.
Isolating the problem
This was my approach to try to find out what’s wrong:
- Replicate the problem. We’ll setup skinny applications of both Rails 3.2 and Rails 4.5.2 to investigate and run the code above to make sure we get similarly shitty results.
- Eliminate Ruby as a culprit. Lots of ways to do this, most simply I’ll compare my benchmarks with that obtained in a pure ruby environment (ie
irb
, no Rails). - Dig into the
Range
andEnumerator
classes. There are lots of ways to do looping in Ruby, are they all benchmarking so poorly between versions? This should help us figure out which docs we need to dig into. - Dig into documentation between versions to figure out what’s happening.
Setting up an environment
From here, I would suggest opening three terminal tabs:
- One tab in each of our new rails applications. Run
rails c
to enter the console. - One tab running
irb
for pure ruby comparisons.
Setting up our environments
Making sure we can replicate the results:
Summary: Rails 3.2 and No Rails (irb
) are approximately equal, Rails 4.5 is ~4 times slower. All three environments are running the same version of Ruby, so this is definitely a Rails issue.
Is it just Enumerator?
Running (0..5).each.class
returns an Enumerator
object. I decided to test whether other loops/objects would produce similar slowdowns. Here is a sample of some of the tests I ran and their results:
So it seems the damage might be limited to the Enumerator
class. Since this class can be created in a number of ways, lets see if all objects of class Enumerator
are experiencing the slow down, regardless of how they were created:
Very surprising. Both [].each
and 6.times
create an Enumerator object, yet neither experience the major slow down that we’re experiencing when instantiating an Enumerator
object from the Range
class (ie (0..5).each
).
This is a big hint. Since both irb
and Rails 3.x are not experiencing the same slow down that’s happening in Rails 4, and the Array
and Fixnum
classes seem to be working well it’s reasonable to assume that Rail’s Range#each
method has been overwritten. ActiveSupport is the Rails library that extends these methods. Lets take a look.
ActiveSupport
In rails/activesupport/lib/active_support/core_ext, switching to version 4.2.5, we find range/each.rb. First, note that if we switch to version 3.2 on GitHub there’s now each.rb
file - good sign. Here’s the code:
Notice the line alias_method_chain :each, :time_with_zone
? From the documentation, the following are equivalent:
This means that the Range#each
instance method is being overwritten presumably with a much, much slower version, since:
Final test
Lets jump into our Rails-free console (irb
) and see if we can duplicate the problem. First lets get a baseline benchmark:
Next, lets overwrite the the Range#each
method in the same way that ActiveSupport is doing. Note that I’ve commented the call to the private function ensure_iteration_allowed
and replaced alias_method_chain
with its functionally equivalent two-line version described above:
Finally, lets run our benchmark:
Again, our benchmark has gone from 0.39 seconds to 1.23 seconds. We’ve found our culprit.
Conclusion
It’s possible that a large amount of the slow down experienced by 500px is caused by ineffiencies created by the update to ActiveSupport and in particular the Range#each
instance method. Creating an Enumerator
object from a range is a very common procedure.
As long as a comprehensive test suite is in place, it would be interesting to replace this method with its previous version to compare benchmarks in a production application to assess how much of the slow down it causes. As for the decrease in response times that is not explained by this change, lets hope the Rails Core Team places more emphasis on benchmark testing in future versions.