This post was co-written by Jemma Issroff.
There’s a new gem in town! It’s a memoization gem called
MemoWise (pun very much intended) and we think it’s the best way to memoize methods in Ruby. This post covers its origin story, and we also wrote about what makes it so fast and some esoteric Ruby we learned along the way.
What is memoization?
One of our codebases has a method that we’ll call
slow_value. It’s really complex, involves multiple database queries, and takes a long time to return a result.
Performance data for our application made it clear that this method was a bottleneck. It was repeating the same slow computations over and over again even though it was always producing the same result (i.e., it’s deterministic).
Memoization is a solution to exactly this problem! The premise of memoization is to store results on the first computation, and then on subsequent calls, return the stored results instead of re-computing them. Notably, this means that the memoized method must be deterministic.
An easy way to memoize
Our first approach was to use an instance variable and Ruby’s
The first time the method is called,
nil, so the right-hand side of the
||= operation will execute. The result will be stored in
@slow_value, and the next time the method is called it will be returned without executing the rest of the method (as long as
@slow_value’s value is “truthy”). This sped up our code a lot!
Uh-oh! Problems with this approach
There are a few downsides to this approach, however.
What if the method itself returns
nil for some input? Then,
@slow_value would be set to
slow_value again, we would see that
nil, which is a “falsey” value in Ruby, and so the right-hand side of the
||= would be executed every time. (The same problem occurs with
false.) In this case, our code isn’t actually memoizing and we’re just as inefficient as when we started.
What if the method we’re memoizing takes arguments? Then we can’t simply use
||=. One solution here would be to use a hash instead, and key it by the arguments. This quickly becomes cumbersome, especially if we have to do it everywhere we need memoization.
Also, this use of instance variables can itself lead to problems because we now have a method and an instance variable with the same name that can easily be confused. What if a caller accidentally references the instance variable instead of the method? What if we rename the method but forget to update the name of the instance variable? What if the name of the method already matches the name of an instance variable used elsewhere in the class for an entirely different purpose? (This is probably unlikely with
slow_value, but it can easily happen with names like
value. This would lead to our method returning incorrect data, or even breaking an unrelated piece of code.) There are so many ways this can go wrong!
Why write a new gem?
For our codebase, we needed a way to memoize methods that:
- Retrieved values efficiently — we sometimes perform the same memoized lookup thousands of times in performance-critical code paths
- Worked with frozen
- Worked with module, class, and instance methods
- (Bonus) Allowed presetting and resetting memoized results
There was no one gem that matched all of these needs, so we wrote our own! With our new
MemoWise gem, memoization is now as easy as:
Please try it out! (We’re happy to accept contributions!) Every change is extensively benchmarked, and it’s really fast.