Beware of ActiveSupport::Memoizable

“Memoization” is a very useful technique to keep your code efficient and well-organized.  Back when I first learned it (from the NeXT/Apple WebObjects framework), people called it “lazy load” or “lazy load with caching”, but “memoization” seems to be what people, or at least ruby people, call it these days.

Basically it means don’t calculate or fetch something until the first time you ask for it, and then once you do calculate/fetch it, cache it so on the second time you ask for it you’ll use the stored value. Useful for ‘expensive’ operations (for me, more often fetching from an external source than calculation), and useful because it keeps your code well-organized instead of dumping a bunch of fetches/calculations in a constructor or what have you.

In OO, you usually do this when you want it cached for the lifetime of whatever object it lives in, so you don’t need to worry about invalidating the cache (except sometimes you do).

So one way to do this in ruby that you see a lot is something like:

def foo
  @foo ||= actually_get_the_value 
end

One problem with this is that it won’t succesfully cache a nil or false value, because it says “if @foo is ‘falsey’ (nil or false), then set foo to actually_get_the_value”.  But if actually_get_the_value returns false or nil, it’ll keep thinking it needs to be reloaded, which is a bit annoying.

Somewhere around Rails 2.2 or 2.3, ActiveSupport included it’s own module to do this kind of memoization, and in a way that take care of memoizing/caching nil/false values too. How convenient!  I think I learned of it maybe from this blog post, or a blog post like it, and I thought, gee, how convenient, my rails app neccesarily depends on ActiveSupport anyway, I’ll just use that in my own code, it’ll take care of nil/false values, it makes the code elegant and clean and readable on the page, if it’s in Rails it must be a good implementation, right? Seems to have been a popular thought, you can find such use in various code and such suggestion in various places.

I think I was quite wrong, and this was a bad idea.

 So recently I’ve been in horrible debugging hell trying to figure out why my app converted from rails2 to rails3 has such terrible performance.  Trying to figure this out is not something I’m great it, I’m using ruby-prof, but the results aren’t always making sense to me, and end up really, um, volatile (GC issues). But anyway as I was doing this, I got a hint in ruby-prof that I should be suspicious of ActiveSupport::Memoization, it was showing up in stack traces and with timing that seemed odd.

So I googled around, and found this blog post casting some aspersions on ActiveSupport::Memoizable:

  • Memoizing this method made it 14.5% slower.
  • Rails memoization was 57.8% slower than classic memoization (i.e.@cached_content_length ||= @env['CONTENT_LENGTH'].to_i). Alternatively phrased: classic memoization was more than twice as fast as Rails’.
  • The very act of mixing in Memoizable (without memoizing any methods) incurred an 8.3% speed penalty on method execution.

Now, I’m still not completely sure ActiveSupport::Memoizable was part of my problems. But getting rid of it seems possibly to have helped (sorry, I don’t have good benchmarks to share, I’m going crazy with this stuff right now), and certainly didn’t hurt. I was using it kind of a lot — I had a couple different helper/presenter class that memoized several of their methods, and there could be several dozen of these objects active to generate a response.

And what did I need it for anyway?  You want to deal with caching nil/false values too? This idiom works fine:

def foo
  unless defined? @_foo
    @_foo  = stuff
    @_foo.more_stuff(stuff)
  end
  return @_foo
end

Back how I did it in with WebObjects code like 10 years ago, that’s not really so bad.

You lose the ability (which I never used) of ActiveSupport::Memoizable to ‘un-memoize’ everything memoized in an object at once. But you gain the ability (which I occasionally wanted and ActiveSupport::Memoizable didn’t have) to un-memoize a single value easily. “self.remove_instance_variable(:@_foo)”.

You see the “@foo ||= whatever” idiom so much (taking a variables falsey-ness as proxy for it’s existence), that you forget that “defined?” exists, huh?  Sometimes it’s the right tool. That idiom is not really that unreadable, it’s not worth the worry/risk of using ActiveSupport::Memoizable with it’s added magic; I think replacing all those Memoizable’s with this ordinary ruby approach resulted in a noticeable performance improvement, although I don’t have the valid measurements, sorry.

Moral may be, just because something is in Rails core doesn’t neccesarily mean it’s actually written well or performant, or performs well in as many use cases as it’s semantics might apply, or will perform well in your use case.

One thought on “Beware of ActiveSupport::Memoizable

Leave a comment