The Story Behind Ruby 1.9.3 Getting 36% Faster Loading Times
Xavier Shay is an Australian Rubyist who shares an issue with most of us: slow loading Rails 3 apps on Ruby 1.9.2! Unlike most of us, he put together a solution for ruby-head (what I'm calling 1.9.3 but isn't technically*) that, in my own tests, slashed 37% off the boot time of my Rails 3.0 app. He shared his work just a week ago. Awesome! But some other developments have occurred since..
* Just because things are in ruby-head doesn't mean they'll definitely make it into Ruby 1.9.3. Pragmatically, though, ruby-head seems to have attracted the 'Ruby 1.9.3' moniker and it makes for a better headline. Just don't get too upset if, for whatever reason, it gets yanked and delayed till Ruby 2.0.. ;-))
Tip: If you're still on 1.8, check out The Ruby 1.9 Walkthrough, a mega screencast aimed at Ruby 1.8.7 developers who want to learn all about what's new, what's gone, and what's different in Ruby 1.9.2 and 1.9.3.
Slow Rails 3.0 Loading Times == A Big Problem
Ruby 1.9.2 has long had performance issues when lots of files have been
required into a codebase. Back in January, Colin Law posted to the Rails Core mailing list about the problem:
There has been an ongoing thread on the RoR talk list about startup time using Rails 3 with Ruby 1.9.2. Do not be confused by the subject of the thread, which mentions 1.9.1, it has moved on to 1.9.2. The gist is that on Ubuntu (and possibly Macs) the startup time when using 1.9.2 can be very much greater then 1.8.7, even with a minimal app. This applies to running tests, migrations, server startup and so on.
Can anyone here throw any light on this?
Rails 3's once-chief prolific superstar, Yehuda Katz, had the best response at the time:
There are things that the C require code does in 1.9 that slow things down. One such example is re-checking $LOAD_PATH to make sure it is all expanded on every require. This is something that should be addressed by ruby-core. I'll open a ticket on redmine if there isn't one already.
It turns out there was already a ticket from October 2010 on Redmine but little had happened in the interim. The compromise, then, was to find ways to make testing fast on Rails 3 with Spork and similar workarounds.
Xavier To The Rescue!
Xavier, like a champ, spent a significant amount of time digging into the problem. He discovered that ruby-head's then-present way of dealing with loading files was woefully inefficient. He noted that require was working like so (in a very simplified and Ruby-fied example - because MRI Ruby is really written in C!):
def require(file) $loaded.each do |x| return false if x == file end load(file) $loaded.push(file) end
Xaiver then set to work making ruby-head use a hash for more efficient lookups. A system he explained like so (again, this is just incredibly simplified pseudocode):
def require(file) return false if $loaded[file] load(file) $loaded[file] = true end
The result was a much faster boot process for apps with lots of
requires going on (like Rails 3 apps) and he released a patch which lots of people loved. I ran my own tests on a Rails 3 app with ~3000 lines of code and about 20 dependent libraries and got a speed up in load times of 37%.
Experimental Folks Only: There's a patch aimed at Ruby 1.9.2-p180 bringing Xavier's ideas back to the implementation you already know and love. Experiment at your own risk!
4 Days Later, A Core Team Patch
Impressed by the improvement, I was going to write a post about Xavier's work and how you could get to using it right away but then, out of the blue, came something straight from the Ruby core team in Japan, a a 26 line patch to load.c by Masaya Tarui.
Masaya's patch took a totally different approach to Xavier's. Whereas Xavier's patch weighed in at over 1200 lines and essentially re-architected Ruby's feature loading process, Masaya's patch smashes a much-needed optimization into the existing code which significantly reduces the number of loop cycles necessary to check whether files have already been loaded or not.
The end result? I ran my tests again and got a 36% drop in Rails 3.0 app load time on the same app. So almost as fast as Xavier's patch but from a shorter yet scrappier solution.
I ran this briefly by Xavier on Twitter and he believes that this quick fix won't ultimately fix the problem for "really large apps" and some of his extra benchmarks shared in comments on this ticket seem to indicate as such. So I'm leaving this story a little in the air at the end here. Xavier did a fine bit of rearchitecting but Masaya swooped in with a short "quick fix" that, perhaps, has a lower chance of causing regressions.
Rails 3.0 App Bootup Times
When I was writing this post focused on Xavier's work rather than the new load.c patch, I was going to lead off with benchmarks of the new process. It turns out, though, the story became more interesting, so I've relegated these (new) benchmarks to the end of the post!
I'm no statistician and I grimace at poorly crafted benchmarks along with the rest of you. Trust, though, that all of these results came from a defined process so are more likely to be equally skewed, if at all ;-) They're based on the mean userspace times of the 2nd and 3rd runs of a
time ./script/rails runner "puts 37337" using the specific Ruby version on the specified Rails app (an "empty" Rails 3.0.6 app and a 3000 line, 20 gem "bigger" Rails app).
So, here's ruby-head (with the load.c patch) against Ruby 1.8.7 and Ruby 1.9.2-p180:
ruby-head isn't quite back to Ruby 1.8.7 speeds in terms of requiring lots of files, but it's a significant improvement over 1.9.2 (a 35% improvement on the empty app and 36% on the "bigger" one).
End result? You should be getting faster load times in ruby-head and, certainly, when Ruby 1.9.3 drops, whether or not Xavier's work makes it in. In any case, congratulations are due to Xavier for pushing the issue (coincidence the load.c fix came in 4 days after his big reveal?) and for ultimately making Ruby 1.9 a faster place.
[sponsor] Jumpstart Lab, headed by Jeff Casimir, is a training company specializing in Ruby on Rails. Their classes are usually two days long and while their prescheduled classes tend to be in Washington DC, where they're based, they'll travel anywhere if you have (or can find) at least six attendees.