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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
// 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.