Red Floppy Disk

Fredy Jacob

Memoize Expensive Code

Memoization is a performance optimization where the result of a slow or non-performant piece of code is temporarily stored, and when the expensive code is called again, the stored value is returned.

Instead of …

…repeating potentially expensive calculations:

class OldTimeySweetShop
  def average_sweets_per_jar
    sweet_count / glass_jars.count
  end

  def sweet_count
    glass_jars.sum do |jar|
      jar.count_each_sweet_by_hand
    end
  end
end

Instead…

…use the ||= (or equals) operator to store expensive computations in an instance variable.

class OldTimeySweetShop
  def average_sweets_per_jar
    sweet_count / glass_jars.count
  end

  def sweet_count
    @sweet_count ||= glass_jars.sum do |jar|
      jar.count_each_sweet_by_hand
    end
  end
end

shop = OldTimeySweetShop.new
shop.sweet_count
#=> 4000003
shop.sweet_count
#=> 4000003 # this one will be super quick!
shop.average_sweets_per_jar
#=> 8000 # this uses the cached sweet_count

Why?

In typical Rails applications, I’ve found this pattern to be most useful to optimize expensive database calls and to temporarily cache API requests to external services.

The succinctness of the ||= operator means there is only a small impact on the readability of your code.

Why not?

This ||=-based technique can’t be used if the result of the computation is false or nil, in that case you should use the defined? method to memoize.

A piece of code can be memoized only if calling the code again would have the same output as replacing that function call with its return value.

If you’re looking at method calls with parameters, you can create a memoized lookup table, but that’s more complex than the examples in this article.

Memoizing results of repeated database queries isn’t strictly necessary as Rails does its own caching at the SQL level.

Beware Threads!

You should be careful when using memoization in a high-throughput, threaded Ruby environment. You probably are; sidekiq is threaded and many Ruby web servers are as well.

When your code is reaching outside of the Ruby interpreter (e.g. reusable database connections, file handling, writing to a data store, manually creating background threads) it is possible to introduce race conditions if your code is called by multiple threads at the same time. However… you’re unlikely to be doing this in the course of day to day application, but it is a possible source of bugs. So be aware.

Hat tip to Ivo on this one, he’s resolved bugs in open source projects that demonstrate this issue. You can see his recorded talk on spotting unsafe ruby patterns which dives into the details.

Last updated on January 25th, 2021