ToList or not ToList? That is the question

Utilizar os recursos da linguagem de programação e dos frameworks de forma adequada é fundamental para criação de aplicações performáticas. Algumas operações inadequadas podem ser suficientes para sacrificar até mesmo bons algoritmos. Se tratando de performance, um bom exemplo de equívoco em .NET que pode gerar dores de cabeça está no uso abusivo do método ToList.

O método ToList tem como objetivo materializar uma lista de elementos em memória. Na prática, ele retorna uma nova instância de List<TSource> a partir de um IEnumerable<TSource>. Geralmente, essa é uma operação rápida, porém quando trabalhamos com aplicações onde a performance é atributo de qualidade crítico, entender o impacto de sua utilização é fundamental para evitar o uso equivocado e consequentemente colocar a performance da aplicação em risco.

Vamos consultar a implementação do método ToList, no GitHub da Microsoft e entender como realmente funciona a materialização dessa lista:

public static List<TSource> ToList<TSource>(this IEnumerable<TSource> source)
{
    if (source == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
    }

    return source is IIListProvider<TSource> listProvider ? 
            listProvider.ToList() : new List<TSource>(source);
}

Perceba alguns aspectos importantes sobre o método ToList observando sua implementação:

  • Com o objetivo de otimizar performance, o método tratará de forma diferente coleções do tipo IIListProvider, executando uma implementação específica de ToList para cada tipo de operação (Select, Where, OrderBy, etc). Para coleções que não são do tipo IIListProvider, uma lista é criada passando o enumerable fonte como parâmetro. Independente do tipo da coleção, uma nova instância de List<TSource> é criada e os objetos são copiados para essa nova lista, gerando pressão sobre a memória;
  • Caso o tamanho da lista ultrapasse 85.000 bytes, o impacto negativo é agravado, pois a mesma será alocada na Large Object Heap (LOH), tornando o processo de desalocação de memória mais custoso;
  • Um ponto especial em relação a operações como Select e Where, é que ao chamar o método ToList interno, uma nova instância de List<TSource> é criada sem informar uma capacidade (parâmetro capacity), resultando em um array de 4 posições. Considerando que não é possível redimensionar arrays em .NET, teremos uma pressão sobre o garbage collector, pois toda vez que o limite do array for alcançado, um novo array será criado com o dobro da capacidade do anterior, todos elementos serão copiados para o novo array e o array anterior será descartado.

Meça sempre

Para sabermos o quanto a utilização desse recurso impacta a performance, faremos a avaliação de uma operação que utiliza ToList e outra que não utiliza a partir de um enumerable já criado. Para isso, utilizaremos a versão .NET Core 3.1.14 e a ferramenta BenchmarkDotNet.

Métodos utilizados na comparação:

[Benchmark]
public void UsingToList()
{
    var products = _products
        .Where(x => x.Type == ProductType.Electronic)
        .ToList();

    foreach (var product in products)
    {
        Console.WriteLine("Product name is: {0}.", product.Name);
    }
}

[Benchmark]
public void NotUsingToList()
{
    var products = _products
        .Where(x => x.Type == ProductType.Electronic);

    foreach (var product in products)
    {
        Console.WriteLine("Product name is: {0}.", product.Name);
    }
}

Resultado da comparação com uma coleção de 10.000 objetos e posteriormente uma coleção de 50.000 objetos:



O tempo de processamento não aponta uma diferença significativa nesse cenário, contudo a diferença na necessidade de memória é expressiva. Fica claro o impacto negativo ao se utilizar o método ToList, variando de acordo com a quantidade de objetos pertencentes à coleção e seus tamanhos. É importante destacar que em operações que demandam alta performance ou em infraestrutura com recursos limitados, essa alocação excessiva de memória pode ocasionar em uma OutOfMemoryException, interrompendo a execução da aplicação.

Cuidado com ToList em conjunto de ORM’s

Considerando a análise realizada do método ToList, cuidado com sua utilização em conjunto de ORM’s. Observe o código abaixo:

public IEnumerable<Product> GetByType(ProductType type)
{
    return _dbContext
        .Products
        .ToList()
        .Where(product => product.Type == type);
}

Perceba que neste exemplo estamos materializando todos os objetos em memória, para depois executar um filtro nesta lista materializada. Além do consumo de memória, estamos falando de uma consulta mais pesada no banco de dados e uma pressão maior na rede, já que todos os registros desta tabela serão trafegados para a aplicação. Evite materializar as listas de maneira desnecessária em memória.

Mas então, quando utilizar ToList?

Ao consultarmos a documentação da Microsoft, nos deparamos com a seguinte observação:

A utilização do método ToList pode ser adequada para cenários onde o resultado de um Enumerable será utilizado mais de uma vez, fazendo com que a query evaluation não seja executada múltiplas vezes. Neste caso, o valor retornado de ToList serve como um cache para os resultados da query, evitando processamento desnecessário. Ainda assim é importante observar que em situações onde a query retorna uma grande quantidade de objetos, o uso do ToList pode impactar em uso excessivo de memória, valendo avaliar o trade-off em cada situação de uso.

É essencial conhecer os recursos disponíveis na linguagem de programação, nos frameworks e o impacto de cada escolha dentro do nosso código. Uma simples escolha ingênua pode causar uma enorme dor de cabeça. A documentação da Microsoft, o código de referência do framework e livros como C# in depth e Writing High-Performance .NET Code se tornam obrigatórios quando pensamos em criar aplicações eficientes.

Compartilhe este insight:

Comentários

Participe deixando seu comentário sobre este artigo a seguir:

Subscribe
Notify of
guest
5 Comentários
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Anderson
Anderson
26 dias atrás

Parabéns, ótimo conteúdo!

Patrick Segantine
Patrick Segantine
26 dias atrás

Muito bom!!! Não havia lido algo que me chamasse tanto a atenção para este fato.

Lucas
Lucas
25 dias atrás

muito bom

Renan
Renan
23 dias atrás

muito bom! otima explicacao.

Luiz Antônio
Luiz Antônio
20 dias atrás

Boa Castilho! Ótimo conteúdo.

AUTOR

Raphael Castilho
Desenvolvedor especialista em .NET com experiência em aplicações corporativas de larga escala.

SOLUÇÕES EXIMIACO

Codificação de Software

ESTRATÉGIA & EXECUÇÃO EM TI

Simplificamos, potencializamos 
aceleramos resultados usando a tecnologia do jeito certo.

INSIGHTS EXIMIACO

Confira outros insights de nossos consultores relacionados a esta solução de negócio:

13/07/2021
13/07
2021
25/05/2021
25/05
2021
21/05/2021
Douglas Picolotto
Engenheiro de nuvem, arquiteto de software e especialista em AWS e Devops
21/05
2021

COMO PODEMOS LHE AJUDAR?

Vamos marcar uma conversa para que possamos entender melhor sua situação e juntos avaliar de que forma a tecnologia pode trazer mais resultados para o seu negócio.

COMO PODEMOS LHE AJUDAR?

Vamos marcar uma conversa para que possamos entender melhor sua situação e juntos avaliar de que forma a tecnologia pode trazer mais resultados para o seu negócio.

+55 51 3049-7890 |  [email protected]

5
0
Queremos saber a sua opinião, deixe seu comentáriox
()
x

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

O seu insight foi excluído com sucesso!

O seu insight foi excluído e não está mais disponível.

O seu insight foi salvo com sucesso!

Ele está na fila de espera, aguardando ser revisado para ter sua publicação programada.