Como a múltipla enumeração está matando o desempenho em C# (#csharp #dev #performance)

Overview
Neste post, vamos mergulhar em uma armadilha comum no desenvolvimento C#: a múltipla enumeração. Vamos explorar o que isso significa, por que pode prejudicar o desempenho e como você pode evitá-lo com ajustes simples no seu código.
Imagine que você está em um desses buffets onde você pode comer o tanto que quiser, mas toda vez que você pega a sobremesa, você tem que esperar a cozinha prepará-la novamente. Desagradável, né? É assim que a múltipla enumeração pode parecer para o seu código—processar repetidamente uma sequência quando você realmente só precisa fazer isso uma vez.
Relembrando: IEnumerable vs List
No C#, um IEnumerable representa uma sequência de elementos, é a base para o LINQ e execução deferida. Nestes cenários, os dados reais não são buscados ou computados até você começar a iterar sobre a lista. Essa característica pode ser uma faca de dois gumes.
Em constraste, uma List (ou IList) é uma coleção que armazena elementos na memória. Quando você tem uma List, todos os items já foram computados e armazenados, o que significa que iterar sobre ela é rápido e previsível. Tá bom, eu sei! No seu aplicativo você tem uma lista de 15 toneladas métricas de dados e é impossível carregar tudo na memória, mas segura as pontas um pouco. Vai fazer sentido.
Então, o que é múltipla enumeração?
A múltipla enumeração ocorre quando você itera sobre a mesma sequência IEnumerable
mais de uma vez. Embora isso possa
parecer inofensivo, pode levar a vários problemas:
- Impacto no Desempenho: Se a sua sequência é baseada em uma operação custosa (como uma chamada de banco de dados ou uma computação pesada), iterar várias vezes significa que você está pagando esse custo repetidamente;
- Efeitos Colaterais: Se o iterador da sequência tiver efeitos colaterais (por exemplo, log, modificação de estado), você pode ter consequências indesejadas;
- Inconsistências: Com a execução deferida, os dados subjacentes podem mudar entre as enumerações, levando a resultados inconsistentes.
Exemplo prático
Para ilustrar isso, imagine uma aplicação que faz operações em um banco de dados, buscando produtos como o abaixo:
public record Product
{
public int Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
}
No exemplo abaixo temos um console application simples que está fazendo coisas que normalmente faríamos ao trabalhar com um banco de dados. Só que de uma forma muito muito ruim.
O que esta aplicação faz:
- Conecta a um banco de dados SQLite (em memória);
- Preenche o banco de dados com alguns produtos;
- Busca os produtos (ou seja, cria um
IEnumerable<Product>
); - Ordena os produtos por nome e depois por preço;
- Aumenta o preço em 10% (inflação comendo solta até em código de exemplo! 😅);
- Itera sobre essa lista e imprime cada produto;
- Itera sobre essa lista novamente e imprime cada produto.
Eu adicionei alguns Console.WriteLine
para mostrar o que está acontecendo nos bastidores.
using System.Data;
using Bogus;
using Dapper;
using Microsoft.Data.Sqlite;
using Multiple.Enumeration;
using var connection = GetDataBaseConnection();
SeedTestDatabase(connection);
Console.WriteLine("Messing around with the IEnumerable...");
var products = GetProducts(connection);
products = products.OrderBy(x =>
{
Console.WriteLine($"Sorting by Name: {x}");
return x.Name;
}).ThenBy(x =>
{
Console.WriteLine($"Sorting by Price: {x}");
return x.Price;
}).Select(x =>
{
Console.WriteLine($"Increasing price by 10%: {x}");
x.Price += x.Price * 0.1;
return x;
});
Console.WriteLine("Iterating over it...");
foreach (var product in products)
{
Console.WriteLine($"{product.Name} - {product.Price:C}");
}
Console.WriteLine("Again...");
foreach (var product in products)
{
Console.WriteLine($"{product.Name} - {product.Price:C}");
}
Quer rodar esse código? Você pode conferir ele aqui no meu Github: Multiple Enumeration
Output
Messing around with the IEnumerable...
Iterating over it...
Fetched from DB: Product { Id = 1, Name = Gulgowski - Wisozk, Price = 51.07550073662538 }
Fetched from DB: Product { Id = 2, Name = Hilpert, Ondricka and Stanton, Price = 36.239341080004074 }
Fetched from DB: Product { Id = 3, Name = Cole, Block and Harber, Price = 88.1801337902502 }
Sorting by Name: Product { Id = 1, Name = Gulgowski - Wisozk, Price = 51.07550073662538 }
Sorting by Name: Product { Id = 2, Name = Hilpert, Ondricka and Stanton, Price = 36.239341080004074 }
Sorting by Name: Product { Id = 3, Name = Cole, Block and Harber, Price = 88.1801337902502 }
Sorting by Price: Product { Id = 1, Name = Gulgowski - Wisozk, Price = 51.07550073662538 }
Sorting by Price: Product { Id = 2, Name = Hilpert, Ondricka and Stanton, Price = 36.239341080004074 }
Sorting by Price: Product { Id = 3, Name = Cole, Block and Harber, Price = 88.1801337902502 }
Increasing price by 10%: Product { Id = 3, Name = Cole, Block and Harber, Price = 88.1801337902502 }
Cole, Block and Harber - $97.00
Increasing price by 10%: Product { Id = 1, Name = Gulgowski - Wisozk, Price = 51.07550073662538 }
Gulgowski - Wisozk - $56.18
Increasing price by 10%: Product { Id = 2, Name = Hilpert, Ondricka and Stanton, Price = 36.239341080004074 }
Hilpert, Ondricka and Stanton - $39.86
Again...
Fetched from DB: Product { Id = 1, Name = Gulgowski - Wisozk, Price = 51.07550073662538 }
Fetched from DB: Product { Id = 2, Name = Hilpert, Ondricka and Stanton, Price = 36.239341080004074 }
Fetched from DB: Product { Id = 3, Name = Cole, Block and Harber, Price = 88.1801337902502 }
Sorting by Name: Product { Id = 1, Name = Gulgowski - Wisozk, Price = 51.07550073662538 }
Sorting by Name: Product { Id = 2, Name = Hilpert, Ondricka and Stanton, Price = 36.239341080004074 }
Sorting by Name: Product { Id = 3, Name = Cole, Block and Harber, Price = 88.1801337902502 }
Sorting by Price: Product { Id = 1, Name = Gulgowski - Wisozk, Price = 51.07550073662538 }
Sorting by Price: Product { Id = 2, Name = Hilpert, Ondricka and Stanton, Price = 36.239341080004074 }
Sorting by Price: Product { Id = 3, Name = Cole, Block and Harber, Price = 88.1801337902502 }
Increasing price by 10%: Product { Id = 3, Name = Cole, Block and Harber, Price = 88.1801337902502 }
Cole, Block and Harber - $97.00
Increasing price by 10%: Product { Id = 1, Name = Gulgowski - Wisozk, Price = 51.07550073662538 }
Gulgowski - Wisozk - $56.18
Increasing price by 10%: Product { Id = 2, Name = Hilpert, Ondricka and Stanton, Price = 36.239341080004074 }
Hilpert, Ondricka and Stanton - $39.86
Process finished with exit code 0.
O que há de errado com este resultado?
Olhando para o resultado, você pode ver claramente que todos os produtos foram buscados do banco de dados duas vezes, e todas as operações também foram feitas duas vezes. Este é um claro exemplo de múltipla enumeração.
Isso é obviamente um exemplo extremo, mas serve de exemplo. Em um cenário do mundo real, você pode não ter uma situação tão ruim quanto essa (buscando no banco de dados duas vezes), mas esse tipo de coisa pode facilmente matar o desempenho da sua aplicação.
Como evitar a múltipla enumeração
Para evitar essa armadilha de desempenho, considere as seguintes estratégias:
- Materialize sua Sequência: Converta o IEnumerable para uma List ou um Array usando
.ToList()
ou.ToArray()
. Isso força a execução imediata e armazena os resultados na memória, para que as iterações subsequentes sejam rápidas e livres de efeitos colaterais; - Reutilize o resultado (cache): Se você sabe que precisará enumerar uma sequência várias vezes, armazene o resultado em uma variável em vez de reutilizar a consulta original;
- Seja cuidadoso com o LINQ: Entenda que alguns métodos LINQ (como
.Where()
,.Select()
) usam execução deferida e planeje de acordo.
Esta não é (nem de longe) uma lista com todas as possibilidades, mas já deve servir como um bom ponto de partida. Você precisará levar em consideração o que sua aplicação precisa e qual é a melhor abordagem para o seu cenário específico.
Conclusão
A múltipla enumeração é um assassino de desempenho sutil, mas potente em C#. Ao entender a natureza do IEnumerable e estar atento à execução deferida, você pode escrever um código mais eficiente, previsível e fácil de manter. Então, da próxima vez que você estiver trabalhando com consultas LINQ, lembre-se: uma boa enumeração é melhor do que duas!
Espero ter ajudado!
References: