Melhorando a performance da sua aplicação com Memoization

Escrito em 26 de Outubro de 2021 por Redação iugu

Atualizado em 24 de Agosto de 2023

Criar códigos para relatórios e processamentos complexos pode ser um processo exaustivo, com muitas repetições e códigos longos demais.

É para resolver essas questões que usamos o memoization, que nada mais é do que uma técnica de otimização do código, que pode acelerar a performance ao armazenar resultados em cache, e retornando-os quando requeridos.

Quer entender como aplicar o memoization aos seus projetos? Continue lendo este artigo e aprenda quando e como utilizar essa técnica.

 

O que é o Memoization?

Memoization é uma das formas de otimização do código para que o cache seja o primeiro resultado de uma função, e retorne posteriormente.

Essa técnica evita que um novo processamento seja feito repetidas vezes, aumentando a velocidade da ação.

 

Quando utilizar o Memoization?

O memoization pode ser utilizado para diferentes objetivos, sendo os principais:

  • Queries demoradas; 
  • Processamentos complexos; 
  • Geração de relatórios de diversos formatos; 
  • Funções que nunca mudam o resultado quando recebem o mesmo parâmetro; 
  • Chamadas API remotas; 
  • Entre outros.

Porém, é preciso sempre analisar se o memoization é a melhor opção para otimizar o cache.

 

Como utilizar o Memoization?

Por ser uma técnica aplicada à linguagem de código, o memoization pode ser aplicado a diferentes tecnologias. 

Para ilustrar alguns exemplos do uso do memoization, na prática, vamos utilizar a linguagem Ruby.

 

Exemplo de uso em queries

Vamos pensar em um cenário onde um usuário consiga ver a quantidade de posts criado por ele logo na página inicial, em um dashboard. 

Assim que ele acessar a  página, esta query já será executada e em alguma parte da página será exibido este resultado.

Imagine isto sendo exibido (por algum motivo) em dois, três ou mais lugares (na mesma requisição) e sem a otimização. 

O problema também fica mais complicado se você usa um banco de dados pago por leitura (como o Firebase ou DynamoDB). 

Além de economizar os custos do projeto, o memoization garante que a velocidade de resposta também seja otimizada.

Analise o exemplo abaixo de uma query sem otimização:

class User
has_many :posts
# rodará a query todas as vezes que alguém chamar este método  # mesmo que seja na mesma view (mesma requisição)
def count_posts
posts.count
end
end 

E como conseguimos otimizar esta chamada? Veja abaixo:

class User
has_many :posts
def count_posts
# por convenção, utiliza-se uma variável com o mesmo nome do método  # para armazenar o resultado
@count_posts ||= posts.count
end
end 

Com essa comparação, pode surgir a dúvida sobre o uso do cache ao invés do memoization.

Porém, esse método é mais assertivo, garantindo que não ocorra a invalidação de cache.

 

Exemplo em geração de relatórios 

Além das queries, o memoization também pode ser utilizado para a geração de relatórios.

Utilizando o exemplo abaixo, com um relatório que utiliza a query anterior, é possível tirar algumas conclusões.

A classe utilizada será hipotética, para demonstrar o erro, na prática. Portanto, não use esse modelo para o seu código! 

class Report
# Extrair o cabeçalho da função logo após a solicitação 

 # não é a melhor opção, pois a função será executada

 # duas vezes (ou mais, quando por método).
# Além disso ainda podemos ter outro problema em que os relatórios tenham algum resultado
# diferente, pois o usuário pode estar usando o sistema e inserindo os dados  no momento.
def create_xls
headers = process.headers
data = process.data
... gera o xls e envia para algum lugar
end
def create_pdf
headers = process.headers
data = process.data
... gera o pdf e envia para algum lugar
end
private
def process
...
end
end 

E como resolvemos esse caso? Assim como no exemplo anterior, basta usar o memoization.

class Report
def create_xls
headers = process.headers
data = process.data
...
end
def create_pdf
headers = process.headers
data = process.data
...
end
private
def process
@process ||= ...
end
end 

Depois de um processamento demorado, é possível gerar um PDF ou um XLS, por exemplo, com a mesma fonte de dados e sem divergências.

 

Dicas para usar o Memoization

Até o momento vimos algo relativamente simples e, sim, podem existir casos mais complexos que tenham entrada de parâmetros. 

Para demonstrar isso, vamos usar novamente a classe User, mas com um novo desafio: utilizar um método com parâmetro. 

class User
has_many :posts
def posts_order_by(field: :created_at)
posts.order(field)
end
end 

A função acima pode parecer completa, mas não está!

Vamos relembrar que o memoization é uma técnica de otimização que, basicamente, faz cache do primeiro resultado de uma função e retorna isso posteriormente. 

Portanto, se deixarmos a função como está, não vamos ter o resultado esperado. 

Veja a seguir o problema e como podemos resolver.

user = User.find(id)
posts_by_created_at = user.posts_order_by # Se tem valor default, não precisa informar.
# posts_by_created_at
# Title Created At
# D 01/01/2021
# C 02/01/2021
# B 03/01/2021
# A 04/01/2021 

Qual vai ser o resultado se chamarmos o método, passando o :title como parâmetro, depois que já tiver chamado o mesmo método que ordena pelo campo :created_at ? 

user = User.find(id)
posts_by_created_at = user.posts_order_by # Se tem valor default, não precisa informar.
posts_by_title = user.posts_order_by(field: :title)
# posts_by_created_at
# Title Created At
# D 01/01/2021
# C 02/01/2021
# B 03/01/2021
# A 04/01/2021
# O resultado se repetiu, então vamos entender onde está o erro.
# posts_by_title
# Title Created At
# D 01/01/2021
# C 02/01/2021
# B 03/01/2021
# A 04/01/2021 

Sabemos agora que o memoization usado de forma errada pode nos trazer problemas. 

Então vamos para a solução, que pode parecer um pouco estranha (e talvez seja mesmo), mas resolve completamente este problema.

Para isso, vamos adicionar a variável field dentro de @posts_by  def posts_order_by(field: :created_at).

class User
has_many :posts
@posts_order_by ||= Hash.new do |hash, key|
hash[key] = posts.order(field)
end
@posts_order_by[field]
end
end 

Agora com o código certo, vejamos como ficaria o resultado. 

user = User.find(id)
posts_by_created_at = user.posts_order_by # Se tem valor default, não precisa informar.
posts_by_title = user.posts_order_by(field: :title)
# Conseguimos listar pela data de criação, como esperávamos.
# posts_by_created_at
# Title Created At
# D 01/01/2021
# C 02/01/2021
# B 03/01/2021
# A 04/01/2021
# Ordenou corretamente pelo título, como deveria.
# posts_by_title
# Title Created At
# A 04/01/2021
# B 03/01/2021
# C 02/01/2021
# D 01/01/2021

Para entender por que a função está correta, acompanhe a explicação abaixo:

class User
has_many :posts
def posts_order_by(field: :created_at)
# Fizemos memoize de um Hash novo
@posts_order_by ||= Hash.new do |hash, key|
# É neste bloco do Hash.new que está a mágica.
# Primeiramente, ele tenta encontrar a chave que estamos acessando,  # e se por algum motivo não existir, caímos neste bloco.
# É repassado para nós o próprio hash e a chave em questão, ou seja,  # se caiu aqui é porque não fizemos memoization da chave e temos  # que executar a query e criar sua chave.
# Neste caso, ele cria o hash[:created_at] e na segunda chamada  # ele cria o hash[:title]
hash[key] = posts.order(field)
end
# Buscamos a chave :created_at e depois a :title
@posts_order_by[field]
end
end 

A partir disso, sabemos que, se adicionarmos um novo parâmetro, provavelmente teremos um novo problema. 

Porém, há uma solução para que o memoization funcione em uma função com dois parâmetros.

class User
has_many :posts
def posts_order_by(field: :created_at, sort: :desc)
@posts_order_by ||= Hash.new do |hash, key|
hash[key] = posts.order(field.to_sym => sort)
end
# Uma das opções é gerar uma chave única para a combinação de parâmetros.  # Aqui você não necessariamente precisa fazer o que mostraremos,  # você pode fazer adaptações.
@posts_order_by["#{field}-#{sort}"]
end
end 

Alterando bem pouco do código, já podemos ter uma versão funcional com mais de um parâmetro. 

 

Resolução de nil em Memoization

Infelizmente nem tudo são flores. Temos ainda alguns “problemas” com o memoization e é exatamente quando ele executa algum processamento e retorna nil. 

class User
has_many :posts
def last_post
@last_post ||= posts.last
end
end 

Caso seja preciso chamar o método last_post mais de uma vez (imaginando que nosso usuário não tenha nenhum post), o memoization não será eficiente. 

Uma vez que ele vai procurar algum valor em @last_post e como está nil, ele vai executar a query todas as vezes que for chamado e é neste ponto que começamos a ter dor de cabeça com o memoization, pois temos que fazer mais checagens a fim de evitar esse problema.

Uma das soluções seria esta abaixo, mas é mais complexo de se tratar e cobrir os casos que podem retornar nil. 

class User
has_many :posts
def last_post
# Imagine isso se repetindo diversas vezes no seu projeto.  # Sem contar que você pode esquecer de fazer isso e gerar problemas futuramente.
return @last_post if defined? (@last_post)
@last_post ||= posts.last
end
end 

Mas claro, ninguém vai repetir isso em todas as funções. Nada que um helper, concern ou algo parecido não consiga resolver nesses casos. 

 

Conclusão sobre o Memoization

O memoization pode ser uma ótima solução para otimizar códigos específicos, que podem ser repetitivos e longos demais. Porém, é preciso usá-lo com cautela.

Como vimos neste artigo, o memoization pode trazer alguns problemas quando usado de forma errada ou precitada. 

O memoization também pode causar uma sobrecarga no armazenamento, para salvar os arquivos temporários, principalmente se usado com memória RAM. 

Porém, a depender do projeto, o memoization é uma boa saída para economizar tempo e custos. Fica a seu critério (e das necessidades do projeto) usar ou não essa técnica.

Posts relacionados