Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Making ActiveRecord 2x faster (tenderlovemaking.com)
158 points by tenderlove on Feb 19, 2014 | hide | past | favorite | 44 comments


It's good to see members of the Rails core team taking performance seriously. I've been on a personal performance quest for the last couple of years, and what I've discovered is that the Rails community as a whole (not necessarily the core developers, but just people producing Rails-related software as a whole) really seems to consider performance to be something of an afterthought. There is a lot of low-hanging fruit laying around that could substantially improve quality of life for the entire Rails ecosystem if people would put a little time into profiling their stuff and eliminating hotspots.

It's been very discouraging to me to see responses to performance issues that range from lukewarm to plain "I don't care". The Rails community could benefit a lot from a visible push by the core team to make performance a priority.


> seems to consider performance to be something of an afterthought

Not an afterthought, but I'm more interested in providing features for my customers. The site is fast enough, and the time to develop new features is more important than making the thing scale for zillions of users when I only have 100's hitting the web site.

I don't ignore performance, but if you're charging enough, it's certainly not the primary consideration.


I think that people building end-user software are less culpable in some regard. My comment is aimed more at people producing gems and libraries and frameworks that then propagate through the Rails community. When they are slow, they cause a compounding effect that ripples down to the developer building end-user applications, who just assume that 400ms for a page is "normal", because that's how it's always been.

Ideally, in the Rails world, the people building client apps would only have to worry about the algorithmic complexity of their code, and the libraries should be operating efficiently under the covers. To that end, you're completely right in that you shouldn't have to worry about performance - the people writing the software you build on should have already have done that. But when they don't, that costs you time and money, and I think that's a shame.


I think the main reason for this is that there's no 'convention' to profiling a gem. No one's built something that's super simple, off the shelf and easy to use. If someone did, and it got enough traction, it could be as much a part of the gem building process as bundler or jeweler is right now.


ruby-prof has been around forever. Combine it with kcachegrind and you have a very powerful toolset. I wrote about instrumenting Rails apps here: https://www.coffeepowered.net/2013/08/02/ruby-prof-for-rails...

The same concept applies to gems. You just construct a scenario that stresses your gem internals, run it with ruby-prof, pop open the resulting dump in kcachegrind or similar, and explore to find the hotspots. I did something like this for MongoMapper, and documented the process here: https://www.coffeepowered.net/2013/07/29/mongomapper-perform...


It can be less about scalability and more about user experience. I have several apps that I only need one or two dynos to run, but I invested in caching/performance anyway because it's a better experience for the (few, high-paying) clients who are using them.


yea this attitude is a BIG part of the reason i switched to Java. If you learn some common Java design patterns (ahem... program to interfaces) then all of a sudden the magic of Ruby doesn't seem to add that much (in fact the data-binding in Java usually simplifies form validation). So then with a huge boost in performance for free, all of a sudden hosting costs are far lower and less billable hours are put to testing cache expiration.

That said, I will still probably end up migrating toward Clojure/cljs and js-focused apps...

EDIT: It's a waste to bleed a client if all your blood-money is going to Heroku! XD


This is a serious question, though a tad off-topic: what tools do you use in the Java world?

I've tried to get into the Java web world, but it's always felt like the tools weren't what I was looking for. Partly, it always feels like the Java world builds so many ways to do things that it's hard to get a feeling on what is canonical. Even with something like Play, should I use Java or Scala? If I use Java, should I use Ebean or Hibernate? If I use Hibernate, should I use XML or. . .

In some ways, it feels like Java advocacy is focused on non-coding managers. Or maybe I just don't know where to look for developer-focused advocacy.

For example, in the C# world, Microsoft has done a decent job telling developers how to get started and why they should care about .NET MVC. In the Java world, it's hard to know where to get started: Spring, Play, Ninja, Wicket, Dropwizard, Tapestry, Wicket, etc.

I guess I'd love to hear your recommendations and experience in moving to Java.


I think it really depends on your goals. You can look at JEE for enterprise standards , JSF, JAX-RS, EJB, JPA or look to the spring stack which may be a easier to work with such as spring mvc, spring and some mix of spring jdbc or spring data. I think in general you can look at how a project like Play integrates these various frameworks and start from there.


it's funny i probably won't give that much actual tech advice here but will relate a little anecdote. I was once asking in a forum about moving from RoR to Clojure, trying to calm my nerves, and some Clojure dev straight up asked me "Out of curiosity.... why is it that a lot of web developers think that Clojure isn't an adequate solution for lightweight apps?" and I remember saying something like "Well.... I think because it's not like a full-fledged framework with built-in ORM and a prescribed workflow" etc. etc. and as I was saying it I had this epiphany -- a lot of the JVM guys are so used to figuring out tooling and library integration that while the rest of the world is looking in and saying "How do you start from scratch like that?" a lot of them are looking out and saying "How come you guys are trying to solve a variety of problems all using a single tool?". Code is code, and a lot of web programming is just accumulating strings.

So yeah I'd say I'm pretty early in my endeavors (I mean I code using a Java tool for a corporation but there is a pretty strict workflow for that) but as far as open source & my actual education is concerned, ok I'll give you tech advice --

short answer: https://spring.io/guides

1. Java has a number of MVC solutions. Pick one & study it, probably with a tutorial/guide. Don't focus on the amount of configuration available (the cruft), just follow a tutorial to get a few hello world kindof pages. You'll see that it's not nearly as difficult as one might imagine. I first studied JSF, then Spring MVC (in retrospect, Spring MVC is probably a better starting point. I think it is also really hot in the job market.)

2. Pay attention to the terminology (research every unknown acronym) and see how it is an analogue to something you're familiar with.

3. Make note of the features/points that are not available in the language/framework you currently use. For me, data-binding to a form was the first thing where I thought "woah thats so much simpler...". Look up PrimeFaces or something like that if you want to see an explicit/obvious example of this, though personally I'm not sure I'd use it in practice (Spring itself offers data-binding stuff).

4. Research some JVM framework that is similar to one you are already familiar with (Play is a good start here or again Spring MVC if you're not afraid of Java syntax) and figure out how to make some basic pages, then add some logic.

5. Study the relationships between servlets/classes/beans/etc, figure out how they are scoped. A lot of it is pretty much free-form code and frameworks like Spring will let you do pretty transparent things before you get deeper into more nuanced app-flows.

6. Protocol is usually pretty easily determined by app needs. I like to default to json since I do a lot of javascript and it seems to fit better with REST, but internally lots of java apps & web services use XML. This hasn't really hurt my brain at any point yet.

7. And finally, when in doubt, try a pure Java solution until you realize that you need a tool. I think this has really been key for me. I could build apps on a similar level to what I was doing in RoR without many fancy libraries, and now I'm learning enough of each that I can make a more educated decision when the application actually demands it. I think all the choices can be daunting but the power of the minimal tools is enough that the entire chain isn't necessary for every application.

8. Oh, plus one more! If you like some other language (for me it's Clojure) study the language in it's pure form plus throw in a little study of Java interoperability in case you'd like to use it sparsely within one of the tried & true Java frameworks. This may not be possible without some meditation on it...

There are lots of templating languages available so as always you'll often just be injecting stuff into html. Many frameworks are just turning into a home base to shove fancy javascript frameworks into so depending on your app, you may not even need much server-side code (in fact I think this is why some of the simplistic monolith frameworks are surviving so long).

And of course, I don't know everything.... so maybe the JVM isn't the answer. I'm just having a lot of fun learning it, and I was once a Rails guy.


Seconded!


The problem being, of course, that you're then writing Java. :)

I still really like Ruby for its expressiveness and malleability, and don't plan to move away from it any time soon, but a lot of that means digging into these libraries, finding problems, and fixing them. I really enjoy that process, but it's rather inefficient for me to be responsible for the performance of every library in my stack.

Ruby has a reputation for being slow, and while it's true that you're not likely to ever reach statically-typed language speeds, it can be pretty decently fast. The danger with tons of abstraction is that a performance hotspot buried three libraries down can turn your otherwise-fast application into a tarpit. If Ruby library developers made performance analysis and optimization a goal alongside "beautiful code" and "DRY", Rails applications across the board would get substantially faster without changes to the underlying VM.

For example, Sprockets uses Pathname heavily. Ruby's stdlib Pathname is extremely robust and extremely slow. This slows down path resolution for every single asset quite a bit. My custom-patched Sprockets does asset resolution and compilation around 43% faster than stock Sprockets. As you can imagine, this has tangible benefits in the speed of my development cycle, and substantially increases my happiness. I opened an issue on the Sprockets project, but it was closed as requiring too-substantial a refactor. Imagine how many developer-hours would be saved if the Sprockets maintainers were to fix that hotspot?


I was really curious to see the pull request, so I hunted it down: https://github.com/sstephenson/sprockets/issues/506

Edit: if you just want to install a gem and skip the why, the parent has this blog post: https://www.coffeepowered.net/2014/02/09/faster-pathname/


It's worth noting that I don't really like faster_pathname as a solution. It's a cudgel that monkeypatches the stdlib Pathname. A proper solution would be a Pathname subclass that Sprockets uses directly, allowing for customized behavior without potential side-effects in other systems.


you're right on about this stuff. someone once told me "yeah rails is great except the libraries have fugly internals"

but it's trueeee I prefer scripting languages to Java but there are a few of them available on the JVM. To be honest, I think the right balance is in the dynamic-OPTIONALLY-static typed languages.

Java interoperability solves this in a way... I've been taking a look at Dart for similar reasons. I just don't think the answer for me is RoR. Once I learned a Lisp (Clojure) and a lot of javascript I realized a lot of the syntax features I like about Ruby exist in many other places

What RoR really has going for it is the gem community, but if they're hard to trust and come with a slew of other problems...? Tough choices


I'm on the Dart team, and we've heard a lot of positive feedback from Rubyists. Come join us on #dartlang or misc@dartlang.org if you have questions. We'd love to hear what someone thinking of migrating from RoR needs.


"Ruby has a reputation for being slow"

Web services that DO use Ruby today are almost always IO bound. For web services that don't require data crunching CPU bound processes, Ruby as a language is hardly the bottleneck.


Don't confuse being IO bound for not having CPU overhead. Being IO bound just means that your app is going to hit a concurrency ceiling due to IO before you hit full saturation on all your cores. It does not mean that a given request can't be faster. Despite the fact that our apps aren't CPU-bound, a few years ago, I patched some performance problems out of Haml and shaved multiple dozens of milliseconds off of each of our requests. That translates into faster page renders, snappier responses, and happier users.

While faster view renders might not improve your theoretical maximum throughput, it will certainly have a positive impact on your users' experience.


Very strange to have a programmer culture that is afraid of thinking about performance.

It's treated like a hobgoblin.


something of an afterthought

Cf. the comical performance of the entire rubygems ecosystem (no least bundler).

A programming language community can hardly make a stronger statement about their general mindset wrt performance than tolerating such an unmitigated trainwreck at the very core of their ecosystem and _every single developers_ daily workflow.


Bundler's kind of an interesting example. Its slowness stems from the fact that the semantics of "require" are ambiguous, so it resorts to a conservative-and-expensive search mechanism in order to preserve those semantics.

If you could magically drop support for Ruby 1.8, and make every library use require_relative to load its internals rather than require, I'm pretty sure most of the problems with Bundler would disappear.

Gem developers: If you aren't supporting 1.8, switch to exclusively using require_relative to load your app internals. Consider separate 1.8/post-1.8 code blocks for requiring code. If you'd like to understand why, strace a bundler-backed app load sometime.


so it resorts to a conservative-and-expensive search mechanism in order to preserve those semantics.

Sorry but come on, these apologisms are part of the reason why these things never get fixed. Ruby 1.8 went end-of-life two years ago. And which precious ambiguousity are we enshrining here that could possibly justify this magnitude of wasted developer lifetime?

There is no excuse. If anyone in charge gave a flying f*ck and was not a complete code illiterate we'd have a --fast flag by now. In fact we'd probably not even have it anymore because it would have become the de facto default over night...


Wow, there's some hostility there. The bundler team has accepted performance pull requests from me before - I'm sure they'd be happy to do the same if you were to add this --fast flag. Perhaps you should show them what code literacy looks like.

The issue is the require means "search the load paths for files matching this filename, filename.rb, filename.so, then search relative to the requiring file for the same if nothing was found". Bundler manages loads by finding all the gems in the bundle (which have been pre-resolved and locked), then creating a load path which satisfies the dependency order. So, when you `require "foo"`, the (extensively large) load path is iterated looking for a match. Given the number of gems (and thus the number of items in the load path) in a typical project, it's not difficult to recognize why this might be slow. Over the course of a load, you will perform hundreds or thousands of require invocations, each of which has to perform this iterative search.

This seems unnecessary, except you have to know how to resolve ambiguities. Consider something like this file: https://github.com/rails/docrails/blob/master/activesupport/...

What does that require statement mean? It's a require statement that asks Ruby to require something called 'benchmark', from within a directory that contains a 'benchmark' file. There are, in fact, multiple matches for "benchmark" in the load path. In order to ensure that the right one is loaded, you have to perform that iterative search, in order, to make sure that you always arrive at the stdlib "benchmark" before the ActiveSupport core_ext "benchmark". Yet, in the majority of cases, by convention, when you require some file in a library, you're actually wanting to require some file relative to your requiring file. It's obviously inefficient to iterate the entire load path looking for some file that you know lives in a subdirectory relative to the requiring file, but since Ruby doesn't have any way of knowing whether you mean "load the first hit of this file from the load path" or "load the file relative to the requiring file", it has to perform the slower, more conservative search.

This is what require_relative is for. That lets you tell Ruby "Hey, skip all the load path shenanigans and just load this file relative to the requiring file". Since you don't have to do the extensive load path iteration, the search time is constant, rather than linear.

I've experimented with a few "--fast" systems, including pre-emptive load path iteration and caching (doesn't really make a difference; you aren't going to beat stat by any significant margin), and load path "baking" (for each file loaded in the manifest, keep the resolved absolute path and replace relative paths with absolute paths"), but I've yet to come up with a solution that properly replicates Ruby's search-and-load behavior, and invariably ends up breaking on some edge case. Fuzzy semantics are kind of a bitch like that.


Wow, there's some hostility there.

Yes. I haven't interacted with the bundler team but I did try to contribute to rubygems during their security debacle. I hope these are not the same people.

but I've yet to come up with a solution that properly replicates Ruby's search-and-load behavior, and invariably ends up breaking on some edge case.

I have a hard time believing that'd be your only bottleneck?

If that's really all then do exactly what you said. Break the edge case, replace your iterative search with the O(1) lookup. There you have your --fast flag. Document that require_relative is to be used for relative imports for the flag to work.

Then count the days until someone patches up a 'fast-rails'.

And to the gem that auto-converts projects into --fast compatibility.


Do you have any references to posts/articles talking about 'require_relative' and 'require' being a performance issue for bundler?

Hadn't personally heard of this before.


Is caching taking performance seriously? The cache everything approach keeps people from asking "What is it doing? Why is it taking so long when I'm only doing X, Y, and Z?"


Absolutely - caching is a staple of performant code. I agree with you that caching can mask other more fundamental problems, but it's a critical component of any performant codebase. "Don't duplicate unnecessary work" is rule #1. The Rails community has that mantra as its foundation in regards to programmer work (DRY is a wonderful principle), but seems to be content to not expect that same principle to apply to libraries.


Quoting @tenderlove: "I think we can make the next release of Rails (the release after 4.1) the fastest version ever."

I'm glad to see the gradual progression since 3.2 stable!


Props for Aaron's continuous work on mature optimization of Ruby & Rails. I recall his yeoman's work on Ruby dtrace probes and Rails initialization time.


I applaud anyone trying to improve performance. It is discouraging, though, that it only helps with some of the fastest and most efficient queries: direct SELECTs by ID without any joins or LIKE statements.

For those interested in good caching without bypassing parts of ActiveRecord, Memcache + Dalli (gem) is a great solution. Caching is not always the right solution, though. Sometimes the best choice is reversing how you get to that data or optimizing your query directly using SQL.


these particular optimizations aren't caching any data from the database.

They are only caching calculations internal to AR. So you don't need to worry about using cached data when the actual external data has changed, is one thing.

The caching here is only of things that can't possibly change, essentially the output of a function for a given input.

Your memcache suggestions are a different sort of thing entirely, which certainly may be helpful for some apps in some circumstances.


Exactly. Use both (with caveats), they sit at different levels.

It's like

   cloudflare/varnish/nginx -> rack -> rails -> app
                                          -> memcache

Anything you can use to take load off the DB, which in turn may hit some sort of blocking I/O call (SSD or HDD) is a very Good Thing (TM). Or a sleeping heroku dyno. ;)


The optimizations being discussed in OP do not take any load off the db. They only reduce CPU time for AR to calculate what it's going to ask the DB for.

Unless I'm misunderstanding.


Isn't the goal that the refactored AR code is basically cleaner, which allows AR query caching to be more mergable?

Not asking for the same thing twice (or N-1 or N^2-1 extra times) if has to go over the network is a good thing.

Databases often aren't properly configured OOTB, especially when it comes to temporary tablespace and query caching.


Seriously, have you read the OP? The optimizations in the OP do not prevent AR from asking for the same thing more than once. They do not effect how many times AR asks for things at all, as far as they can tell. It is _not_ caching database responses. It is caching internal AR calculations only, and does not reduce the number of times AR asks for things from the DB. Unless I am misunderstanding.


I am skeptical as to whether the calls speeded up by this optimization form a sufficient portion of the actual call time of any actual real world apps actions, such that this optimization will have any non-trivial effect on a real world app.

It might. But I definitely wouldn't assume it does. Micro-optimization.

Of course, it doesn't hurt either way, unless it does in code complexity, or opportunity cost.


This seems to be but a first step - it's currently limited only to calls of the form `Article.find(id)` and `Article.find_by_xxx(value)`.

e.g. `Article.where(xxx: value)` or `Article.where(xxx: value).last(3)` will not benefit from this improvement yet.


It's possible to do that in AdequateRecord, but I've purposely left the mechanism undocumented because the API is not stable. You can do something like this:

  statement = undocumented_method_call { |params|
    Article.where(xxx: params[:xxx])
  }
  statement.execute xxx: "some value"
https://github.com/rails/rails/blob/e5e440f477a0b5e06b008ee7...

The trick is that the cache only supports caching ActiveRecord::Relation objects, and not everyone knows which calls will return a Relation object. For now, I want to hide this API and make it as "automatic" as possible so that people don't have to change app code, but still get performance boosts.


Isn't the find_by_XXX style (soon to be?) deprecated?


There has been a lot of confusion about this, but this is not true[1]. `find_by_xxx` and `find_by_xxx!` are not part of the deprecation.

1. http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.ht...


Nope: http://edgeguides.rubyonrails.org/4_0_release_notes.html#act...

That one is staying, most of the others are going (e.g. find_all_by_xxx, find_last_by_xxx)


Something else to look forward to in 2014 for Rails performance, along with the planned arrival of JRuby 9k !


It appears the 2x speedup is from by-passing ActiveRecord::Relation


Will this work for Rails 3.2?




Consider applying for YC's Winter 2026 batch! Applications are open till Nov 10

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: