2 pontos por GN⁺ 2024-10-20 | 1 comentários | Compartilhar no WhatsApp
  • 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> com TryGetSpan() ao percorrer arrays ou List<T>, reduzindo o custo de iteração
  • TryGetSpan() identifica TSource[] e List<TSource> por comparação de tipo, mas a abordagem de capturar o array interno de List<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() e Sum()
  • 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() como IEnumerable<int> e depois executa Count, All, Any, First, Single e Last para comparar .NET 8 e .NET 9
  • Ele usa BenchmarkDotNet, e o projeto deve mirar net8.0;net9.0 e 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ção
    • LinqAny: de 17,096.735 ns para 2,483.927 ns, de 32 B alocados para nenhuma alocação
    • LinqFirst: de 15,289.747 ns para 2,243.341 ns, de 32 B alocados para nenhuma alocação
    • LinqSingle: de 21,684.114 ns para 4,884.329 ns, de 32 B alocados para nenhuma alocação
    • LinqAll: de 10.588 ns para 2.562 ns, de 32 B alocados para nenhuma alocação
    • LinqLast: 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 um ReadOnlySpan<T>, permitindo uma iteração mais rápida
  • O código central de desvio verifica source.GetType() == typeof(TSource[]) ou source.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))
  • 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á-lo
  • CollectionsMarshal.AsSpan(Unsafe.As<List<TSource>>(source)) obtém um Span<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 Enumerable que envolvem iteração adiada, como yield, têm dificuldade para depender dessa otimização
  • O próprio nome System.Runtime.CompilerServices.Unsafe evidencia esse risco

Escopo das chamadas a TryGetSpan()

  • Com o NDepend, foi feito um scan de System.Linq.dll para verificar chamadores diretos e indiretos de TryGetSpan()
  • 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 Enumerable tentam 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ção
    • AppendSelectLast: de 4,122.007 ns para 2.661 ns, de 144 B alocados para nenhuma alocação
    • DefaultIfEmptySelectElementAt: de 4,090.818 ns para 5.724 ns, de 144 B alocados para nenhuma alocação
    • RangeUnionFirst: de 66.309 ns para 6.193 ns, de 344 B alocados para nenhuma alocação
    • ListSkipTakeElementAt: de 6.268 ns para 2.916 ns
    • RangeReverseCount: de 11.024 ns para 6.134 ns
  • Por outro lado, SelectWhereSelectSum ficou 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() e Sum(), otimizações adicionais se tornam possíveis
  • Por exemplo, OrderBy(criteria).First() pode ser otimizado para executar como Min(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 cadeia Where(...).Select(...) em uma lista como um único iterator
  • Esse iterator é criado no Select() override de ListWhereIterator<TSource, TResult>
  • ListWhereIterator<TSource> é criado quando Enumerable.Where() verifica que a origem é List<TSource>
  • ListWhereSelectIterator<TSource, TResult> não faz override de métodos como TryGetFirst() ou TryGetLast()
  • 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 _predicate e _selector são chamados juntos

Caso de IListSkipTakeIterator<TSource>

  • IListSkipTakeIterator<TSource> é um iterator especial criado quando aplicável
  • MoveNext() usa _state - 1 como índice de base zero da lista
  • Ter um campo de índice separado seria mais legível, mas o bias é armazenado em _state para reduzir o tamanho dos campos do iterator
  • A otimização desse iterator está em não percorrer desnecessariamente elementos fora do intervalo de _minIndexInclusive a _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

 
GN⁺ 2024-10-20
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 de IEnumerable
    Antigamente 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

    • Eu também prefiro o aspecto funcional das extensões LINQ para IEnumerable e IQueryable
      É 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
    • Eu também uso LINQ dessa forma
      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/
    • Sempre usei LINQ apenas com a sintaxe de métodos
      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 for e lógica C# comum e depois comparar com a implementação em LINQ, para ver os prós e contras das duas abordagens
    • Se você gosta de Haskell, talvez também goste de outros usos da sintaxe de consulta do LINQ, como composição de parsers combinadores
      A sintaxe de consulta não é hardcoded exclusivamente para IEnumerable; esse é apenas o comportamento padrão, e ela pode ser usada em quase qualquer lugar
      Funciona de forma um pouco parecida com sobrecarga de operadores
      [1]: https://github.com/acple/ParsecSharp/blob/da8d0cb9ec39e28dd9...
    • Se a sintaxe LINQ desaparecesse amanhã, eu não sentiria muita falta, mas composição funcional é realmente poderosa e também mais fácil de manter
  • 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.dev ou docs.rs
    A 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

    • Dá até para brincar que a Microsoft investiu na OpenAI porque ela é o único jeito que faz sentido de navegar pela documentação dos pacotes .NET/NuGet
      Mas, de forma assustadora, também acho que isso está perto da verdade
    • Agora a documentação da Microsoft inclui links diretos para o código-fonte do método que você está vendo
      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
    • Concordo, mas parte do motivo provavelmente é que C# open source é algo relativamente recente
      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
    • O Sandcastle Help File Builder existe há muito tempo e, pelo que lembro, começou como um projeto interno da Microsoft, mas estranhamente poucas bibliotecas o usam
      https://github.com/EWSoftware/SHFB
    • Também há uma forma de escrever testes ao lado do código: https://clipperhouse.com/go-test-csharp/
      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 lambdas
    Além disso, deveria ser possível usar tipos elevados como IEnumerable e Option como o último item de uma expressão LINQ, em vez de select ...
    Em certos casos de uso, select cria sobrecarga desnecessária e também limita coisas como expressões LINQ recursivas em cauda
    Bibliotecas que apostam tudo em LINQ, como a minha, mas não usam IEnumerable, IQueryable nem extensões LINQ, continuam sendo ignoradas
    Porque 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, SelectMany e Where, mas também com coisas como GetAwaiter
    Em 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/

    • Se houver feedback útil, é melhor abrir uma issue ou enviar um PR em dotnet/runtime
      Muitas das melhorias de desempenho do LINQ abordadas no texto entraram dessa forma
    • A biblioteca parece muito interessante, mas também parece, em certa medida, configurada de antemão de um jeito que facilita que ela seja ignorada
      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 responsabilidades
      Mas 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 Result que parece mais ou menos semelhante a Option
      Mas, 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

    • É interessante como esse tipo de conversa aparece com frequência na comunidade .NET
      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
    • Também queria muito ter tipos de unidade de medida
      Isso tornaria a manutenção de código de engenharia ou ciência muito mais fácil
    • Com OneOf[0] e Dunet[1], já dá para introduzir uniões discriminadas com bastante facilidade
      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
    • É um segredo aberto que o F# é o campo de testes para recursos do C# e do VB.NET, e figuras oficiais como Hanselman já citaram isso várias vezes
  • 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

    • Amigos, não se viciem em LINQ
      O LINQ vai prender vocês e fazer com que ressintam ambientes que não têm LINQ
    • Ainda assim, ele é menos poderoso do que algo como polars
  • 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

    • A novidade quente hoje em desenvolvimento web com .NET é o Blazor, mas fora da bolha dos blogs da Microsoft ele não é muito popular, e não parece que vá ser
      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”
    • Recentemente comecei a me interessar por desenvolvimento web com C#
      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...
    • É um pouco fora do mainstream, mas a combinação de F# e Fable é muito poderosa
      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
    • Aprendi fazendo, mas vou listar alguns materiais que podem servir de referência
      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 UI com renderização no servidor, procure materiais que usem Razor e, no começo, é melhor evitar materiais sobre Blazor
      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

    • Esses atributos correspondem à biblioteca de benchmarking usada no artigo
      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
    • Não sei que código .NET você está olhando
      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 como Min(criteria), pode ser útil
    Mas, 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