Melhorias de desempenho do LINQ no .NET 9.0
(blog.ndepend.com)- O .NET 9.0 reduz significativamente o tempo de execução em vários cenários comuns de LINQ em relação ao .NET 8 e, em alguns benchmarks, elimina até as alocações
- Uma das principais melhorias é obter um
ReadOnlySpan<T>comTryGetSpan()ao percorrer arrays ouList<T>, reduzindo o custo de iteração TryGetSpan()identificaTSource[]eList<TSource>por comparação de tipo, mas a abordagem de capturar o array interno deList<T>como um span é uma otimização da família Unsafe que pode ser invalidada quando a capacidade muda- O LINQ do .NET 9 reconhece cadeias de chamadas comuns, cria iterators especiais e aplica otimizações adicionais em métodos terminais como
Count(),First(),Last(),ElementAt()eSum() - Apenas migrar e recompilar já permite obter algumas melhorias de desempenho do LINQ, e também estão incluídas otimizações como uso de SIMD e detecção antecipada de sequências vazias
Por que percorrer arrays e listas ficou mais rápido
- O primeiro benchmark armazena
Enumerable.Range(1, 10_000).ToArray()comoIEnumerable<int>e depois executaCount,All,Any,First,SingleeLastpara comparar .NET 8 e .NET 9 - Ele usa BenchmarkDotNet, e o projeto deve mirar
net8.0;net9.0e ser compilado em modo Release - No .NET 9, o tempo de execução de vários métodos cai bastante e as alocações também desaparecem
LinqCount: de 16,198.490 ns para 3,043.563 ns, de 32 B alocados para nenhuma alocaçãoLinqAny: de 17,096.735 ns para 2,483.927 ns, de 32 B alocados para nenhuma alocaçãoLinqFirst: de 15,289.747 ns para 2,243.341 ns, de 32 B alocados para nenhuma alocaçãoLinqSingle: de 21,684.114 ns para 4,884.329 ns, de 32 B alocados para nenhuma alocaçãoLinqAll: de 10.588 ns para 2.562 ns, de 32 B alocados para nenhuma alocaçãoLinqLast: de 15.967 ns para 6.918 ns
A diferença que TryGetSpan() faz
- A principal causa do ganho de desempenho é o uso de
TryGetSpan() - Se o enumerable a ser percorrido for um array ou uma lista,
TryGetSpan()retorna umReadOnlySpan<T>, permitindo uma iteração mais rápida - O código central de desvio verifica
source.GetType() == typeof(TSource[])ousource.GetType() == typeof(List<TSource>)e então obtém o span- Arrays são tratados com
Unsafe.As<TSource[]>(source) - Listas obtêm um span do array interno com
CollectionsMarshal.AsSpan(Unsafe.As<List<TSource>>(source))
- Arrays são tratados com
- No código,
source.GetType()é chamado duas vezes e não se usa uma verificação de null após o casting, mas essa foi uma escolha feita por especialistas de desempenho do .NET considerando otimizações do compilador C# e do JIT - Em uma stack .NET altamente otimizada, micro-otimizações podem produzir resultados diferentes do que parecem à primeira vista
Restrições de CollectionsMarshal.AsSpan()
List<TSource>referencia internamente um array e, quando a capacidade da lista precisa aumentar ou diminuir, cria um novo array e passa a referenciá-loCollectionsMarshal.AsSpan(Unsafe.As<List<TSource>>(source))obtém umSpan<TSource>desse array interno- Se a capacidade da lista mudar de alguma forma, o array obtido por essa abordagem pode ser invalidado
- Por causa dessa restrição, algumas operações de
Enumerableque envolvem iteração adiada, comoyield, têm dificuldade para depender dessa otimização - O próprio nome
System.Runtime.CompilerServices.Unsafeevidencia esse risco
Escopo das chamadas a TryGetSpan()
- Com o NDepend, foi feito um scan de
System.Linq.dllpara verificar chamadores diretos e indiretos deTryGetSpan() - O caminho do assembly analisado é
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0-rc.1.24431.7\System.Linq.dll - A partir de
TryGetSpan(), foi gerada uma consulta de código para identificar os chamadores, e 56 métodos correspondentes foram exportados como grafo de dependências - Muitos métodos padrão de
Enumerabletentam fazer iteração via span quando a coleção é um array ou uma lista - No entanto, como a abordagem de prender o array interno da lista não é segura, ainda há limitações para operações que exigem execução adiada
Otimizações baseadas em iterators especiais
- O segundo benchmark usa um caso presente no PR Consolidate LINQ’s internal IIListProvider/IPartition into base Iterator class
- Os alvos de teste incluem
Distinct().First(),Append().Select().Last(),Reverse().Count(),DefaultIfEmpty().Select().ElementAt(),Skip().Take().ElementAt(),Union().First(),Select().Where().Select().Sum(), entre outros - No .NET 9, algumas cadeias de chamadas ficam extremamente mais rápidas
DistinctFirst: de 65.318 ns para 11.192 ns, de 328 B alocados para nenhuma alocaçãoAppendSelectLast: de 4,122.007 ns para 2.661 ns, de 144 B alocados para nenhuma alocaçãoDefaultIfEmptySelectElementAt: de 4,090.818 ns para 5.724 ns, de 144 B alocados para nenhuma alocaçãoRangeUnionFirst: de 66.309 ns para 6.193 ns, de 344 B alocados para nenhuma alocaçãoListSkipTakeElementAt: de 6.268 ns para 2.916 nsRangeReverseCount: de 11.024 ns para 6.134 ns
- Por outro lado,
SelectWhereSelectSumficou mais lento, saindo de 3,959.622 ns no .NET 8 para 4,460.008 ns no .NET 9, e manteve a alocação de 112 B
Reconhecimento de cadeias LINQ comuns
- A equipe de desempenho do .NET projetou o código para reconhecer cadeias de chamadas LINQ comuns
- Quando uma cadeia específica é detectada, é criado um iterator especial que processa o fluxo de trabalho de forma mais eficiente
- Quando a cadeia termina em métodos como
Count(),First(),Last(),ElementAt()eSum(), otimizações adicionais se tornam possíveis - Por exemplo,
OrderBy(criteria).First()pode ser otimizado para executar comoMin(criteria)
Estrutura de Iterator<T> e classes derivadas
- Internamente, o LINQ tem a classe base abstrata
Iterator<T>e 40 classes derivadas - Todas essas classes são aninhadas dentro da classe
Enumerable Iterator<T>é uma classe abstrata, mas seus métodos são virtual, então as classes derivadas fazem override apenas dos métodos necessários- Essa estrutura serve como base para comportamentos especiais por cadeia de chamadas
Caso de ListWhereSelectIterator<TSource, TResult>
ListWhereSelectIterator<TSource, TResult>trata a cadeiaWhere(...).Select(...)em uma lista como um único iterator- Esse iterator é criado no
Select()override deListWhereIterator<TSource, TResult> ListWhereIterator<TSource>é criado quandoEnumerable.Where()verifica que a origem éList<TSource>ListWhereSelectIterator<TSource, TResult>não faz override de métodos comoTryGetFirst()ouTryGetLast()- O ponto central da melhoria de desempenho é combinar a cadeia muito comum
Where(...).Select(...)em listas em um único iterator, em vez de dois- Dentro de
MoveNext(), os dois delegates_predicatee_selectorsão chamados juntos
- Dentro de
Caso de IListSkipTakeIterator<TSource>
IListSkipTakeIterator<TSource>é um iterator especial criado quando aplicávelMoveNext()usa_state - 1como índice de base zero da lista- Ter um campo de índice separado seria mais legível, mas o bias é armazenado em
_statepara reduzir o tamanho dos campos do iterator - A otimização desse iterator está em não percorrer desnecessariamente elementos fora do intervalo de
_minIndexInclusivea_maxIndexInclusive
Otimizações adicionais obtidas apenas com migração
- No .NET 9, vários cenários comuns de LINQ ficaram mais rápidos
- O trabalho necessário para aproveitar as melhorias de uma nova versão do .NET é migrar e recompilar
- O LINQ também é otimizado de outras formas
- Em casos como soma de sequências de inteiros, usa SIMD onde possível
- Sequências vazias têm menor custo de enumeração graças à detecção antecipada
- Os vídeos DeepDotnet podem ser vistos como material de aprendizado de .NET com Scott Hanselman e Stephen Toub
1 comentários
Opiniões do Hacker News
Acho que a parte mais útil do LINQ não é a estrutura de extensão baseada em árvores de sintaxe do
IQueryable, nem a sintaxe embutida na linguagem, mas sim os métodos de extensão deIEnumerableAntigamente isso era chamado, de forma um tanto confusa, de “LINQ to Objects”, e permite escrever C# de forma concisa em estilo funcional
O artigo original trata principalmente das otimizações desses métodos de extensão
Só depois de aprender Haskell é que esse modelo fez sentido para mim, e ele também compartilha algumas das vantagens e armadilhas do Haskell, como avaliação preguiçosa
Se usado indiscriminadamente, pode resultar em código difícil de entender e lento; por isso, eu não recomendaria se não houver na equipe alguém que conheça idiomatismos funcionais básicos e avaliação preguiçosa
IEnumerableeIQueryableÉ mais fácil de raciocinar e, em lugares como o Entity Framework, nem sempre é a opção mais rápida, mas em geral é uma alternativa bem decente
Também gosto de usar Dapper em vez de EF
Dito isso, projetos C# tendem a acumular camadas de abstração absurdas, e desenvolvimento “enterprise” em geral é doloroso de ver
Há alguns nomes um pouco fora do padrão, mas ele tem tudo o que é necessário
Eric Lippert escreveu uma excelente série de posts explicando mônadas em conexão com LINQ: https://ericlippert.com/2013/04/02/monads-part-twelve/
Não gosto de ter outra linguagem “embutida” dentro da linguagem hospedeira, e o resultado final, de qualquer forma, precisa voltar para C#
Mesmo quando não se usa um ORM como Entity Framework ou Dapper, prefiro deixar a lógica de acesso a dados, incluindo SQL, em um projeto abstraído separado
Assim ela não se espalha pela aplicação inteira e pode ser substituída se outro RDBMS passar a ser necessário
Em 20 anos, isso só aconteceu uma vez na prática
Quando desenvolvedores juniores usam LINQ, colocar um profiler e um debugger junto ajuda a entender o que está acontecendo por dentro
Às vezes também é útil fazê-los escrever primeiro com loops
fore lógica C# comum e depois comparar com a implementação em LINQ, para ver os prós e contras das duas abordagensA sintaxe de consulta não é hardcoded exclusivamente para
IEnumerable; esse é apenas o comportamento padrão, e ela pode ser usada em quase qualquer lugarFunciona de forma um pouco parecida com sobrecarga de operadores
[1]: https://github.com/acple/ParsecSharp/blob/da8d0cb9ec39e28dd9...
Não entendo por que a equipe do dotnet não investe mais recursos e tempo em ferramentas
Precisamos de doctest e geração de documentação, recursos para escrever testes unitários melhores e mais rápidos ao lado do código real, acessibilidade ao código-fonte, um ambiente em que ao apertar F12 não seja preciso descompilar DLLs, e um hub central de pacotes e documentação como
pkg.go.devoudocs.rsA maioria dos pacotes NuGet não tem documentação nenhuma, ou só tem um README no GitHub, ou no máximo uma wiki curta
Outros ambientes como Rust, Go, Java e Python estão anos-luz à frente nesse aspecto
Mas, de forma assustadora, também acho que isso está perto da verdade
Ex.: https://learn.microsoft.com/en-us/dotnet/api/system.string.s...
Para o código-fonte de pacotes NuGet, isso também é fácil se o Source Link estiver ativado, mas ainda é um recurso relativamente novo, então nem todos os pacotes o adotaram
Imagino que a maior parte do código C# ainda seja escrita como código fechado dentro de empresas
Se a Microsoft continuar seguindo a direção mais aberta dos últimos anos, acho que isso vai melhorar com o tempo
Algumas dessas funcionalidades são oferecidas por ferramentas como o Resharper, e fico me perguntando se existe algum acordo explícito ou implícito para não invadir o território um do outro
Sinceramente, a maior parte da documentação que vi em projetos C# era de baixa qualidade, então no fim eu acabava olhando o código-fonte
Minha experiência é que, embora existam muitas ferramentas de autocompletar, elas não ajudam muito na leitura; ajudam mais na escrita
https://github.com/EWSoftware/SHFB
Não sei bem se eu recomendaria
Testei pessoalmente e depois reverti, porque os testes pareciam demorar mais, talvez porque o cache dos artefatos de build tenha piorado
Em vez de “melhorias de desempenho do LINQ”, o correto seria dizer “melhorias de desempenho da própria implementação de
List”A Microsoft parece gastar tempo melhorando as partes de que ela mesma precisa, mais do que melhorias gerais
O LINQ, especialmente a sintaxe de consulta, e não as extensões de método, precisa de investimento
Principalmente na alocação de lambdas e, se possível, na redução de lambdas em tempo de compilação
É necessária uma estratégia para lambdas locais de tipo valor, ou para que a alocação de lambdas não seja uma sobrecarga como é hoje
Variáveis do LINQ também já deveriam ter suporte a curingas (
_), mas isso foi completamente ignorado quando foi introduzido nas lambdasAlém disso, deveria ser possível usar tipos elevados como
IEnumerableeOptioncomo o último item de uma expressão LINQ, em vez deselect ...Em certos casos de uso,
selectcria sobrecarga desnecessária e também limita coisas como expressões LINQ recursivas em caudaBibliotecas que apostam tudo em LINQ, como a minha, mas não usam
IEnumerable,IQueryablenem extensões LINQ, continuam sendo ignoradasPorque a Microsoft se concentra apenas em melhorar o desempenho dos próprios projetos
Um bom exemplo é a inferência de lambdas aprimorada
Ela foi antecipada porque era necessária para as Minimal APIs do ASP.NET Core
Boa parte dos recursos da linguagem e do framework parece ser movida mais por necessidades internas da Microsoft do que por necessidades da comunidade
O pior é que o conjunto de métodos mágicos continua crescendo, não só com extensões LINQ como
Select,SelectManyeWhere, mas também com coisas comoGetAwaiterEm vez de introduzir os recursos de higher-kinded types que realmente seriam necessários para eliminar essa magia, a Microsoft está adicionando funcionalidades para si mesma, principalmente para o compilador
Com isso, tudo permanece com tipagem fraca, e o compilador só consegue descobrir as coisas de forma aproximada
O LINQ é um dos principais diferenciais da linguagem, mas tem sido praticamente deixado de lado desde o C# 3
Ainda verem LINQ apenas como algo útil para percorrer listas, especialmente a própria implementação de listas deles, é realmente uma pena
As melhorias de desempenho em si são bem-vindas e vão ajudar muitos usuários, mas o foco sempre estreito limita o potencial
[1] https://github.com/louthy/language-ext/
dotnet/runtimeMuitas das melhorias de desempenho do LINQ abordadas no texto entraram dessa forma
Há muitas instruções
using, o que não é um grande problema para quem entende como dividir bem um projeto em unidades necessárias e separar responsabilidadesMas a maioria dos desenvolvedores não estrutura projetos assim, e esses pequenos pontos podem virar barreiras para o desenvolvedor médio
Desenvolvedores juniores muitas vezes já têm dificuldade com a sintaxe e os métodos padrão do LINQ, especialmente também em termos de desempenho
É bom que o README mencione isso
Normalmente as pessoas estão ocupadas em “vender” a biblioteca, então gostei de ver escrito de verdade onde ela é forte e qual é seu objetivo
A menção de que ela é não idiomática também pode ser um problema para quem está aprendendo C#/.NET
A Microsoft provavelmente quer que as ferramentas e a linguagem sigam certas práticas, e uma nomenclatura naturalmente alinhada à programação funcional pode ser vista como um obstáculo bem grande quando a Microsoft avalia melhorias
Dei estrela no repositório e tenho bastante interesse no que foi criado
Em algumas aplicações grandes que fiz recentemente, usei um tipo
Resultque parece mais ou menos semelhante aOptionMas, ao olhar de novo a biblioteca, percebi que eu achava que sabia bastante de programação funcional, mas na prática não era bem assim
Sou bastante bom em C# e já fiz coisas complexas, mas programação funcional, mesmo lendo bastante, ainda é difícil de assimilar; F# For Fun And Profit foi o que mais consegui entender
No fim, isso não quer dizer que você esteja fazendo algo errado
A Microsoft vai mirar no desenvolvedor médio ou iniciante, que compõe a maior parte do ecossistema dela
Espero que esta biblioteca consiga obter melhorias internas
Dá para ver que foi investido um tempo enorme, e só o número de estrelas no GitHub já é um sinal suficiente de que há pessoas realmente usando e se beneficiando dela
Desculpe se soou como desdém, mas o trabalho feito é interessante e parece ter documentação suficiente para aprender aos poucos os conceitos que eu ainda não conheço
Quanto mais o C# pegar emprestado do F#, melhor
Estou esperando que uniões discriminadas finalmente cheguem ao C# para que seja possível fazer modelagem de domínio direito
Toda vez, surge a pergunta: “por que não simplesmente usar F#?”
O C# vem tentando alcançar isso há muitos anos
Se o F# vem impulsionando muitas inovações no ecossistema .NET e esteve anos à frente em termos de recursos, fico me perguntando por que não recompensar esse esforço com uso
Se existe uma direção desejada para o desenvolvimento da linguagem, ela deveria ser incentivada por escolhas reais
No ecossistema Java isso funcionou desse jeito, e agora o Java também está melhorando
Quando o mercado cresce, pode surgir um ciclo de feedback positivo em que o esforço de engenharia também aumenta
Depois de ler fóruns por muitos anos, a impressão é que o pessoal do C# quer “simplesmente” ficar no próprio campo e esperar
Parece um pouco tribalista, como se o time fosse “C#”
Não vejo muito essa cultura em outros ecossistemas de linguagens, e fico com a impressão de que, se o F# estivesse em outro ecossistema que não o .NET, talvez já tivesse prosperado há muito tempo
Isso tornaria a manutenção de código de engenharia ou ciência muito mais fácil
Exemplo de uso real: https://chrlschn.dev/blog/2024/07/csharp-discriminated-union...
[0] https://github.com/mcintyre321/OneOf
[1] https://github.com/domn1995/dunet
O que mais sinto falta ao trabalhar em outras linguagens ou ecossistemas é o LINQ
É muito bom ter esse tipo de recurso na biblioteca padrão, e ele foi desenhado de forma elegante dentro das restrições dadas
Há uma seção relacionada naquele artigo anual, do tamanho de um livro, que cobre todas as melhorias de desempenho do .NET 9
https://devblogs.microsoft.com/dotnet/performance-improvemen...
Estranhamente, o HN não permitiu reenviar, então o artigo não chegou à front page e acabou enterrado
Depois que você se acostuma com LINQ e passa a trabalhar em domínios onde ele normalmente brilha, não quer voltar a fazer as coisas de outro jeito
O LINQ vai prender vocês e fazer com que ressintam ambientes que não têm LINQ
Tenho curiosidade se existe algum livro ou tutorial abrangente bom para aprender desenvolvimento web end-to-end com dotnet
A maioria do que encontrei era básica demais, antiga demais ou de baixa qualidade
Pessoalmente, acho que ele vai seguir o mesmo caminho do Silverlight
As tecnologias antigas ainda estão no .NET 9, funcionam e são mantidas
Hoje em dia, fazer desenvolvimento web com .NET geralmente significa criar APIs HTTP/JSON/REST e conectá-las ao framework de frontend que você preferir
No meu caso, uso React ou NextJS
Bons termos de busca são ASP.NET WebApi ou, mais modernamente, ASP.NET Minimal API
Ainda é possível fazer renderização server-side com .NET MVC usando Razor
Como é a linguagem de marcação do ASP.NET MVC, procure por “ASP.NET MVC Razor”
Para o bem ou para o mal, criar aplicações web em .NET parece ter ASP.NET como a resposta de fato
A falta de alternativas é um pouco suspeita, mas enfim
Ouvi um podcast com Andrew Lock, autor de “ASP.NET Core in Action”, e ele pareceu alguém que conhece bem o assunto
Ainda não li o livro, mas pode ser o que você está procurando
1: https://dotnetcore.show/season-6/navigating-the-aspnet-core-...
2: https://www.manning.com/books/asp-net-core-in-action-third-e...
No servidor, você pode rodar Giraffe sobre ASP.NET, uma camada de programação funcional com desempenho semelhante ao C#
No frontend, dá para escrever React com uma linguagem de programação funcional de verdade
Naturalmente, também é possível compartilhar código F# entre frontend e backend
Em livros, há “C# 12 and .NET 8 - Modern Cross-Platform Development Fundamentals - Eighth Edition: Start building websites and services with ASP.NET Core 8, Blazor, and EF Core 8”, de Mark J Price, e “Web API Development with ASP.NET Core 8: Learn techniques, patterns, and tools for building high-performance, robust, and scalable web APIs”, de Xiaodi Yan
Em tutoriais, há as séries do IAmTimCorey e do Shawn Wildermuth no YouTube
Para a combinação de backend .NET com frontend JS, procure materiais que usem Minimal API
MVC também é bom, mas carrega muito peso de compatibilidade retroativa, e foi por isso que Minimal API surgiu
Deve haver uma maneira melhor do que esse monte de macarrão de annotations
Toda vez que vejo código .NET moderno, meus olhos doem
Código de teste unitário e de benchmark costuma mesmo parecer um tanto espaguete
Ainda assim, eu não aprovaria um PR com lógica de negócio real escrita desse jeito
Se você realmente odeia isso, dá para usar coisas como AspNetCore sem tocar em nenhum atributo
Eu quase não uso atributos
A parte de que são possíveis mais otimizações quando a cadeia termina com métodos como
Count(),First(),Last(),ElementAt(),Sum(), e de que, por exemplo,OrderBy(criteria).First()pode ser otimizado para executar comoMin(criteria), pode ser útilMas, para começo de conversa, o correto é escrever um código melhor
Isso é interessante para cadeias geradas dinamicamente, mas, se você faz esse tipo de operação em código escrito manualmente, parece um reforço positivo meio distorcido
É como se a biblioteca reconhecesse um padrão ineficiente e o corrigisse
Espero ao menos que haja algum feedback sugerindo melhorias no código de base