Reasons to use memoization

Memoization: “a generic functional programming technique that you can apply to any pure function, meaning a function without any side effects, which always produces the same result if called with the same arguments.”

https://blog.openreplay.com/forever-functional-memoizing-functions-for-performance/

tldr; Check out the memery and asset_ram gems.

A recent Linked In post on memoization got a lot of questions and some pushback, saying that @var ||= function() is good enough. Here’s an example and my reasons for using memoization.

Proper memoization (as opposed to the ||= short circuit):

  • Handles function parameters.
  • Handles falsey values.
  • Is declarative & expressive, not procedural. It describes what it does, not how it does it. There are many Ruby short-circuit boolean expressions in use. ||= requires an extra couple of steps in human work when checking code:

    (Which bool expr? Coded correctly? The class var is not mutated elsewhere? It is impossible for the expr to be falsey?)

    So in my experience explicit memoization is lower maintenance.
  • Is transparent to client code. When I add memoize, my client code doesn’t change.
  • Avoids pre-mature optimization. I write my functions. After getting performance metrics of ‘hot spots’ in my code, I memoize those functions.
  • Doubles as pre-computation code if needed. Memoization is a lazy-evaluation strategy. But sometimes that means that a user gets hit with the penalty before the result is cached. In that case, simply calling the function at boot-up and ignoring the result pre-computes it.
  • Memoization is an FP strategy. It pays off when you write pure functions and use only mutable data. This is how I code. And this style makes code multi-threading-safe.
  • Adding a gem dependency for memoization is an orthogonal issue. Whether in-house written or third-party, proper memoization is an important tool.

Now, my example: this app shows laws in many jurisdictions such as world.public.law. But Jurisdictions don’t change frequently. And when they do, the app has been rebooted. Here, a class method is memoized, saving a db query per request until the next reboot:

class Jurisdiction < ApplicationRecord
  # ...
  class << self
    # ...
    memoize def find_via(slug:)
      Jurisdiction.find_by(slug: slug)
    end

  end
end

I use the memery gem. It has great documentation that helps explain the value over using plain ||=. It’s worked for me reliably for years.

In addition, I wrote the asset_ram gem for memoizing Rails asset link creation. Here, the savings is partially speed, but mostly allocations. Asset links can be seen as class pure functions: during the life of a Rails app, the “fingerprints” do not change. Yet, the normal call to e.g. #image_tag re-performs the work every time. I’ve attained 17-95% allocation reductions depending on the page design.

Leave a Comment