Dicas para Melhorar a Performance em .NET e C#: Quando o uso do First no LINQ pode ser gargalo
Entender o uso do método First() no LINQ te dá uma compreensão sobre a necessidade de performance em .NET e C#. Muitas vezes consideramos otimizar nosso código para sempre obter a melhor performance. Esse trabalho pode ser inútil, pois o tempo eventualmente diminuído por uma otimização é extremamente irrelevante. Portanto, cuidado pela obsessão por performance, uma vez que tratar de performance muitas vezes gera complexidade e complexidade é custo.
Entretanto, como forma de aprendizado, para melhorar a performance de aplicações desenvolvidas em .NET e C# é preciso entender o que está por trás dos métodos internos que utilizamos. É muito comum nos depararmos com situações que não compreendemos completamente e em particular quero falar do uso do método First() do LINQ.
No código apresentado a seguir, temos um record chamada “Pessoa” que possui uma lista de registros. A lista é inicializada com 13 registros predefinidos, contendo informações como nome e total. A classe também possui dois métodos: PegarComFirst() e PegarPeloIndex().
No método PegarComFirst(), utilizamos o método First() do LINQ para obter o primeiro registro da lista. Por outro lado, no método PegarPeloIndex(), utilizamos o acesso direto pelo índice [0] para obter o mesmo primeiro registro.
O código nos mostra dois métodos que têm o mesmo objetivo de obter o primeiro registro da lista, mas utilizando abordagens diferentes. Vamos explorar e analisar as implicações de desempenho e eficiência dessas abordagens ao trabalhar com uma lista de registros pré-definidos.
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
namespace LinqFirst;
public record Registro
{
public required string Nome { get; init; }
public int Total { get; init; }
}
public class Pessoa
{
private List<Registro> _registros = new(13)
{
new() { Nome = "Maria", Total = 133569652 },
new() { Nome = "José", Total = 77815153 },
new() { Nome = "Antônio ", Total = 35507524 },
new() { Nome = "João", Total = 29887445 },
new() { Nome = "Francisco", Total = 22421466 },
new() { Nome = "Ana", Total = 19963777 },
new() { Nome = "Luiz ", Total = 15418958 },
new() { Nome = "Paulo", Total = 14167689 },
new() { Nome = "Carlos ", Total = 138420110 },
new() { Nome = "Manoel", Total = 133418211 },
new() { Nome = "Pedro ", Total = 99525412 },
new() { Nome = "Francisca", Total = 85359013 },
new() { Nome = "Raimundo ", Total = 82124215 }
};
[Benchmark]
public Registro PegarComFirst()
{
return _registros.First();
}
[Benchmark]
public Registro PegarPeloIndex()
{
return _registros[0];
}
}
internal class Program
{
private static void Main()
{
BenchmarkRunner.Run<Pessoa>();
Console.ReadKey();
}
}

Ao analisar os resultados do benchmark, é possível observar que o método PegarComFirst() levou mais tempo em comparação com o método PegarPeloIndex(). À primeira vista, pode-se pensar que o método First() é mais lento. No entanto, isso não é verdade. O método First(), que acessa o método TryGetFirst(), possui a mesma complexidade assintótica, O(1), uma vez que também realiza o acesso por índice, exceto quando um predicado é utilizado ou quando a lista não é do tipo IList.
Quando acessamos um elemento por índice, como no exemplo utilizando _registros[0], o tempo de acesso é direto e possui uma complexidade de tempo constante O(1). Isso ocorre porque a lista interna mantém uma estrutura de dados que permite o acesso imediato ao elemento desejado, sem a necessidade de percorrer a lista em busca dele. Por outro lado, o método First() precisa realizar algumas validações adicionais, como verificar se a lista é nula, se é do tipo IList, entre outras. Essas validações extras podem aumentar um pouco o tempo de execução em comparação com o acesso direto pelo índice.
Embora as validações realizadas pelo TryGetFirst() sejam relativamente simples e rápidas, elas ainda adicionam um custo de processamento, mesmo que pequeno, em comparação com o acesso direto por índice. Essas verificações podem envolver condicionais e chamadas de métodos adicionais, o que pode levar a uma pequena diferença no tempo de execução em comparação com o acesso por índice direto.
Qual é o código por trás do método First() no LINQ?
O código a seguir é o método First() que está disponível no Github. O código apresentado implementa os métodos First() e FirstOrDefault() da classe Enumerable no namespace System.Linq. Esses métodos são responsáveis por retornar o primeiro elemento de uma sequência, levando em consideração um predicado opcional.
A complexidade do método First() é determinada pela implementação do método TryGetFirst(), que é invocado internamente. A complexidade varia dependendo do tipo de sequência fornecida.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace System.Linq
{
public static partial class Enumerable
{
public static TSource First<TSource>(this IEnumerable<TSource> source)
{
TSource? first = source.TryGetFirst(out bool found);
if (!found)
{
ThrowHelper.ThrowNoElementsException();
}
return first!;
}
public static TSource First<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
TSource? first = source.TryGetFirst(predicate, out bool found);
if (!found)
{
ThrowHelper.ThrowNoMatchException();
}
return first!;
}
public static TSource? FirstOrDefault<TSource>(this IEnumerable<TSource> source) =>
source.TryGetFirst(out _);
/// <summary>Returns the first element of a sequence, or a default value if the sequence contains no elements.</summary>
/// <typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam>
/// <param name="source">The <see cref="IEnumerable{T}" /> to return the first element of.</param>
/// <param name="defaultValue">The default value to return if the sequence is empty.</param>
/// <returns><paramref name="defaultValue" /> if <paramref name="source" /> is empty; otherwise, the first element in <paramref name="source" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source, TSource defaultValue)
{
TSource? first = source.TryGetFirst(out bool found);
return found ? first! : defaultValue;
}
public static TSource? FirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) =>
source.TryGetFirst(predicate, out _);
/// <summary>Returns the first element of the sequence that satisfies a condition or a default value if no such element is found.</summary>
/// <typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam>
/// <param name="source">An <see cref="IEnumerable{T}" /> to return an element from.</param>
/// <param name="predicate">A function to test each element for a condition.</param>
/// <param name="defaultValue">The default value to return if the sequence is empty.</param>
/// <returns><paramref name="defaultValue" /> if <paramref name="source" /> is empty or if no element passes the test specified by <paramref name="predicate" />; otherwise, the first element in <paramref name="source" /> that passes the test specified by <paramref name="predicate" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> or <paramref name="predicate" /> is <see langword="null" />.</exception>
public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, TSource defaultValue)
{
TSource? first = source.TryGetFirst(predicate, out bool found);
return found ? first! : defaultValue;
}
private static TSource? TryGetFirst<TSource>(this IEnumerable<TSource> source, out bool found)
{
if (source == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
if (source is IPartition<TSource> partition)
{
return partition.TryGetFirst(out found);
}
if (source is IList<TSource> list)
{
if (list.Count > 0)
{
found = true;
return list[0];
}
}
else
{
using (IEnumerator<TSource> e = source.GetEnumerator())
{
if (e.MoveNext())
{
found = true;
return e.Current;
}
}
}
found = false;
return default;
}
private static TSource? TryGetFirst<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, out bool found)
{
if (source == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
if (predicate == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.predicate);
}
foreach (TSource element in source)
{
if (predicate(element))
{
found = true;
return element;
}
}
found = false;
return default;
}
}
}
Conclusão
Ao decidir entre utilizar o método First() ou acessar diretamente pelo índice, é importante considerar o contexto e a necessidade específica do código. Se o acesso direto pelo índice for suficiente e não houver a necessidade de validar predicados ou tratar casos especiais, o acesso pelo índice é mais eficiente em termos de desempenho. No entanto, se houver a necessidade de aplicar predicados ou tratar casos especiais, o método First() oferece uma abordagem mais flexível e conveniente.
Honestamente, a menos que você precise de um desempenho extremamente otimizado, a diferença de nanossegundos no tempo de execução não terá um impacto significativo em projetos line of business. É preciso ponderar os atributos de qualidade do seu projeto e avaliar se a obsessão por desempenho realmente trará benefícios tangíveis para o negócio, considerando também a complexidade adicional que pode ser introduzida na manutenção do código.
Sendo assim, a escolha entre desempenho e simplicidade de manutenção deve ser baseada nas necessidades e metas específicas do projeto, levando em consideração os aspectos práticos e reais de sua aplicação no contexto do negócio.
FAQ: Perguntas Frequentes
1. Por que devo considerar o uso do método First() no LINQ para melhorar a performance em .NET e C#?
Ao analisar o desempenho do código, é importante entender o comportamento dos métodos utilizados. O método First() do LINQ é uma opção comumente utilizada para obter o primeiro elemento de uma lista, ela é, sem dúvidas mais intuitiva, mas pouco mais lento – irrelevante – ao se comparar com o acesso por índice. Portanto, compreender como esse método funciona e quando utilizá-lo pode ajudar a otimizar a performance do código.
2. Qual é a diferença entre acessar o primeiro elemento por índice direto e utilizar o método First()?
Acessar o primeiro elemento por índice direto, como no exemplo _registros[0], oferece um acesso direto e de complexidade O(1), ou seja, tempo constante. Já o método First() pode ter uma complexidade semelhante, desde que a lista seja do tipo IList. No entanto, quando predicados ou casos especiais são necessários, o método First() oferece uma abordagem mais flexível.
3. O método First() é sempre mais lento que o acesso direto por índice?
Não necessariamente. O tempo de execução do método First() pode ser levemente maior devido às validações extras realizadas, como verificar se a lista é nula ou se é do tipo IList. No entanto, essas diferenças são geralmente insignificantes e dependem do contexto e da implementação específica.
4. Devo me preocupar excessivamente com a performance ao usar o método First()?
Não é necessário se preocupar excessivamente com a performance ao utilizar o método First(). Em projetos lineares de negócio, a diferença de tempo de execução em nanossegundos é geralmente irrelevante. É importante ponderar os atributos de qualidade do projeto e avaliar se a obsessão por desempenho trará benefícios tangíveis para o negócio em relação à complexidade adicional introduzida na manutenção do código.
5. Como devo escolher entre desempenho e simplicidade de manutenção?
A escolha entre desempenho e simplicidade de manutenção deve ser baseada nas necessidades e metas específicas do projeto. Se o acesso direto por índice atender aos requisitos e não houver necessidade de validações extras, pode ser a opção mais eficiente em termos de desempenho. No entanto, se houver a necessidade de aplicar predicados ou tratar casos especiais, o método First() oferece uma abordagem mais flexível e conveniente. Avalie os aspectos práticos e reais de sua aplicação no contexto do negócio para fazer uma escolha adequada.