Using Ruby memoization

In a service oriented architecture we make many requests to external services. These requests time adds up and slows down the site. To improve our performance I can use the memoization technique to make our pages faster.

memoization is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.

When I program I memoize by setting an instance variable to a request.

  1. @foo || = bar.new(params)

This makes a request once, then sets the value to @foo. This persists as long as the instance of the class.

Recently I faced a problem where a method was being called twice. We’ll call it external_request? method. I pass in foo and bar, then check to see if it returns true. The issue was the method was being called twice. The first call returned true, but the second call would return false.

  1. def external_request?(foo, bar)
  2.     @foo = foo
  3.     @bar = bar
  4.     @foo && @bar
  5. end

 Because I care about the first call I knew I could solve this by memoizing the values.

  1. def external_request?(foo, bar)
  2.     @foo ||= foo
  3.     @bar ||= bar
  4.     @foo && @bar
  5. end

This returned true even after the second call, because I saved the value into the instance variable.  Now something to keep in mind. When you are using this technique you want to be careful not to memoize a class variable.

The danger is that this class variable is shared between all users who are on the same instance(unicorn, dyno, etc). Can you see the problem there? If user A makes a request and returns true, then every time the method is called this will return true even if it should be false for other users. This will persist until the unicorn is switched or we refresh the server.

How did I solve the above problem? By making a new method that calls external_request? and sets a cookie during the first method call.

  1. def make_request
  2.   if self.external_request?(foo, bar)
  3.     cookie[:external_request] = 1
  4.   else
  5.     true
  6.   end
  7. end
  8. def self.external_request?(foo, bar)
  9.   foo && bar
  10. end

The lesson is that memoization is great for performance, but we need to be careful what we memoize. In almost every case you don’t want to memoize the class.

This post was inspired by Jon Phillips.