Memoization in Rails

Discover various techniques to memorize and improve the performance of the Rails application

Tushar Adhao
7 min readSep 3, 2022

While developing an application, there could be a few factors that need your attention and one of them could be, speeding up your methods, which are underperforming and slowing down the application speed.

Sometimes it becomes very frustrating to resolve and optimize the queries associated with the underperforming methods. And that’s why it is better to take account of such slow-performing methods beforehand and do the needful to make the operation fast.

This is when memorization comes into the picture to offer its power to speed-up such underperforming methods. But it is essential to understand when to use it as it could make the situation even more bizarre when used in the wrong case. In this blog, let's understand various cases where we can use memorization and the prices you might need to pay for that.

Before proceeding to explore the topic, let's look into the main sections that we are going to discuss below,

  • Memoization
  • Memoization with nil value
  • Multi-block memoization
  • Memorization using Hashes
  • Memoization with the class variable

So without a further go let’s explore Memoization in Rails and how it improves the performance of the application.

Memoization

Let’s consider a scenario where we have a Post model with some methods as below,

class Post
def sku_name
"Name #{id}"
end
end
# On Rails console
3.0.0 :001 > post = Post.first
3.0.0 :002 > post.sku_name.object_id
=> 106340
3.0.0 :003 > post.sku_name.object_id
=> 106400

As we can notice that each time thesku_name method gets called, the memory allocation is different. The same could happen with a situation like calling post.comments a lot of times in consecutive methods of a request so as to develop multiple queries. To avoid such cases, we can memorize these methods as below,

class Post
def sku_name
@name ||= "Name #{id}"
end
end
# On Rails console
3.0.0 :001 > post = Post.first
3.0.0 :002 > post.sku_name.object_id
=> 106280
3.0.0 :003 > post.sku_name.object_id
=> 106280

So after memorization, memory allocation is performed only once and the next time the data is assigned, the value @name will be utilized instead of recalculating a new value. So basically it works as when the first time method gets a hit, it will store the value in the instance variable and the next time when the method gets a hit, instead of re-evaluating the value, it will utilize the saved value from the instance variable as below,

@name = (@name || "Name #{id}")

Note: Though it is a good example to understand the memoization concept but not adviced to memoize values with string (or string with simple interpolation) as it won’t add much performance enhancement (unless the string interpolation has some heavy operation to perform)

Caught expression after reading the note

Memoization with nil value

What if the memoized object has a nilvalue assigned to the instance variable? So the method gets the first call it will evaluate the value (in our case it is nil), since it has a nil value, next time the method gets a call it will re-evaluate the value to be nil , and every time the method gets a call, it will re-evaluate so as to diminish the use of memorization.

def self.blank_post
@blank_post ||= Post.find_by(title: "")
end
# On Rails console
3.0.0 :001 > Post.blank_post
Post Load (0.6ms) SELECT "posts".* FROM "posts" WHERE "posts"."title" = $1 LIMIT $2 [["title", ""], ["LIMIT", 1]]
=> nil
3.0.0 :002 > Post.blank_post
Post Load (0.6ms) SELECT "posts".* FROM "posts" WHERE "posts"."title" = $1 LIMIT $2 [["title", ""], ["LIMIT", 1]]
=> nil

To resolve this issue, there could be multiple solutions to it and one of them could be as below,

def self.blank_post
return @blank_post if defined?(@blank_post)
@blank_post ||= Post.find_by(title: "")
end
# On Rails console
3.0.0 :001 > Post.blank_post
Post Load (0.5ms) SELECT "posts".* FROM "posts" WHERE "posts"."title" = $1 LIMIT $2 [["title", ""], ["LIMIT", 1]]
=> nil
3.0.0 :002 > Post.blank_post
=> nil

Multi-block memoization

So this is the case when a muti-line block needs memorization as given below,

def self.get_users_with_comments
@something ||= begin
posts = Post.includes(:comments).where.not(title: nil)
comments = posts.map{ |post| post.comments }.flatten.compact
users = comments.map(&:user)
end
end

Again here is the same issue for nil value can occur and the same solution over it can be implemented as discussed in the above section or a different approach can be implemented as below,

def self.get_users_with_comments
return @something if @validator

@validator = true
@something ||= beginMemoization
posts = Post.includes(:comments).where.not(title: nil)
comments = posts.map{ |post| post.comments }.flatten.compact
users = comments.map(&:user)
end
end

Memoization using Hashes

There could be various lookouts for using memorization depending upon the situation it has been used. It becomes tricky in situations where values could be changing for a method being memorized as below,

def self.auther_posts_count(auther_id)
Post.where(user_id: auther_id).count
end

In such situations where a dynamic value cannot be memoized since the method takes some dynamic value, we can make use of the Memoist gem, which comes out of the box, or alternatively, we can make use of hashes to solve the problem as below,

def self.auther_posts_count(auther_id)
@post_count ||= {}
@post_count[auther_id] ||= Post.where(user_id: auther_id).count
end
# On Rails console
3.0.0 :001 > Post.auther_posts_count(1)
Post Count (6.4ms) SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = $1 [["user_id", 1]]
=> 1
3.0.0 :002 > Post.auther_posts_count(2)
Post Count (0.7ms) SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = $1 [["user_id", 2]]
=> 0
3.0.0 :003 > Post.auther_posts_count(1)
=> 1
3.0.0 :004 > Post.auther_posts_count(2)
=> 0

Here when auther_id is provided to the method, it will check if @post_count exists else assign empty hash and then add a Hash object with the key will be dynamically provided auther_id and value is the calculated count. Next time if the same auther_id is provided when the method is called then it won’t calculate instead it will provide the same Hash value.

Hashes could be used in many wonderful ways. In our case, let's take another example of ordering posts with dynamic data as below,

class Post
def self.order_post_by(value)
@result ||= Hash.new do |base, key|
base[key] = Post.order(key)
end
puts @result
@result[value]
end
end

So the basic operation of this method is to order Post records as per the provided value (ex. order by title, created_at, etc.) in ascending order. We have used a Hash so that the data gets cached for dynamic attributes provided to the method. The operation includes a simple process that @result will consist of hash data with keys as dynamically provided value and its value will be the ordered list as below,

# On Rails console
3.0.0 :001 > Post.order_post_by(:title)
{}
Post Load (0.3ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."title" ASC
=> [#<Post:0x0000555fce6584b0 id: 1, title: "Helo", description: "AAAAAAAaaaa", ...]
3.0.0 :002 > Post.order_post_by(:user_id)
{:title=>#<ActiveRecord::Relation [#<Post id: 1, title: "Helo", description: "AAAAAAAaaaa", ...]>}
Post Load (0.8ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."user_id" ASC
=> [#<Post:0x0000555fce4dad40 id: 1, title: "Helo", description: "AAAAAAAaaaa", ...]
3.0.0 :003 > Post.order_post_by(:user_id)
{:title=>#<ActiveRecord::Relation [#<Post id: 1, title: "Helo", description: "AAAAAAAaaaa", ...]>, :user_id=>#<ActiveRecord::Relation [#<Post id: 1, title: "Helo", description: "AAAAAAAaaaa", ...]>}
=> [#<Post:0x0000555fce4dad40 id: 1, title: "Helo", description: "AAAAAAAaaaa", created_at: Tue, 30 Aug 2022 15:59:31.908256000 UTC +00:00, updated_at: Tue, 30 Aug 2022 15:59:31.908256000 UTC +00:00, user_id: 1>]

Note: Here in the example (…) indicates the list could have more data and acts as etc. in short

Yes! Hashes work magically

Memoization with the class variable

As of now, We have seen memorization implemented with the instance variable where the cached value will last till the instance.

def memoized_name
@memoized_sku_name ||= "Name #{rand(60)}"
end
# On Rails console
3.0.0 :001 > post = Post.new
3.0.0 :002 > post.memoized_name
=> "Name 36"
3.0.0 :003 > post.memoized_name
=> "Name 36"
3.0.0 :004 > post2 = Post.new
3.0.0 :005 > post2.memoized_name
=> "Name 31"

As we can notice here, the cached value will get changed as the instance gets changed. If we desire not to work with this flow, then we can make use of the class variable with @@ as given below,

def memoized_name
@@memoized_sku_name ||= "Name #{rand(60)}"
end
# On Rails console
3.0.0 :001 > post = Post.new
3.0.0 :002 > post.memoized_name
=> "Name 2"
3.0.0 :003 > post.memoized_name
=> "Name 2"
3.0.0 :004 > post2 = Post.new
3.0.0 :005 > post2.memoized_name
=> "Name 2"

So by using the class variables we can persist the cached value in memorization.

Let’s explore some more interesting topics about enhancing the performance of the Rails application in the upcoming blogs. Stay tuned!

--

--

Tushar Adhao

Software artist spreading nuggets of coding gold and sometimes philosophy too.