Melhorias de desempenho no dot Net 7

BLOG

Melhorias de desempenho no .NET 7

13 de September de 2022

Segundo postagem no blog oficial da Microsoft em 31 de agosto de 2022, o desenvolvedor Stephen Toub publicou uma atualização de desempenho no .NET 7.

Postagem de Stephen Toub

O desempenho é o foco principal do desenvolvimento, sejam recursos criados explicitamente para ele ou recursos não relacionados que ainda são projetados e implementados com o desempenho em mente. E agora com a atualização do .NET 7 chegando é um bom momento para discutir sobre isso.

Substituição na pilha

A substituição na pilha (OSR) é um dos recursos mais interessantes para o .NET 7.

A ideia por trás é que um método pode ser substituído não apenas entre invocações, mas mesmo enquanto está na pilha e em execução. Além do código de camada zero ser instrumentado para contagens de chamadas, os loops são instrumentados para contagens de iteração. Quando as iterações ultrapassam um certo limite, o JIT compila uma nova versão altamente otimizada desse método, transfere todo o estado local/registro da invocação atual para a nova invocação e, em seguida, salta para o local apropriado no novo método.

Agora no .NET 7, podemos evitar em grande parte as compensações entre inicialização e taxa de transferência, pois o OSR permite que a compilação em camadas seja aplicada a todos os métodos, mesmo aqueles de longa duração. Vários PRs foram habilitados para isso, incluindo muitos nos últimos anos, mas toda a funcionalidade foi desativada nos bits de envio.

Graças a melhorias como dotnet/runtime#62831 que implementou suporte para OSR no Arm64 (anteriormente apenas suporte x64 era implementado), e dotnet/runtime#63406 e dotnet/runtime#65609 que revisou como as importações e epílogos OSR são tratados, dotnet/runtime #65675 habilita OSR e como resultado DOTNET_TC_QuickJitForLoops por padrão.

Foi também melhorada a taxa de transferência. Embora a compilação em camadas tenha sido originalmente concebida como uma forma de otimizar a inicialização sem prejudicar a taxa de transferência, ela se tornou muito mais do que isso. Há várias coisas que o JIT pode aprender sobre um método durante o nível 0 que pode ser usado para o nível 1. Por exemplo, o próprio fato de que o código de camada 0 executado significa que qualquer método static acessado pelo método principal será inicializado a cada execução do código de camada 1.

PGO – Otimização guiada por perfil

O PGO (Profile-guided optimization) existe há muito tempo, em várias linguagens e compiladores. A ideia básica é você compilar seu aplicativo, pedindo ao compilador para injetar instrumentação no aplicativo para rastrear várias informações interessantes. Em seguida, você coloca seu aplicativo em prática, executando vários cenários comuns, fazendo com que essa instrumentação identifique o que acontece quando o aplicativo é executado e os resultados são salvos.

O aplicativo é então recompilado, alimentando esses resultados de instrumentação de volta ao compilador e permitindo que ele otimize o aplicativo exatamente como se espera que seja usado. Essa abordagem ao PGO é chamada de “PGO estático”, pois todas as informações são coletadas antes da implantação real, e é algo que o .NET vem fazendo de várias formas há anos.

O PGO dinâmico aproveita a compilação em camadas. Observei que o JIT instrumenta o código de camada 0 para rastrear quantas vezes o método é chamado ou, no caso de loops, quantas vezes o loop é executado. Ele pode instrumentalizá-lo para outras coisas também. Por exemplo, ele pode rastrear exatamente quais tipos concretos são usados ​​como destino de um despacho de interface e, em seguida, na camada 1, especializar o código para esperar os tipos mais comuns (isso é chamado de “desvirtualização protegida” ou GDV). Você pode ver isso neste pequeno exemplo

Primeiro, o PGO agora funciona com OSR, graças a melhorias como dotnet/runtime#61453 . Isso é um grande negócio, pois significa que métodos quentes de longa execução que fazem esse tipo de envio de interface (que são bastante comuns) podem obter esses tipos de otimizações de desvirtualização/inlining. Em segundo lugar, embora o PGO não esteja atualmente ativado por padrão, facilitamos muito a ativação.

Agora é possível simplesmente colocar em seu .csproj e terá o mesmo efeito que se você definir antes de cada invocação do aplicativo, habilitando o PGO dinâmico. Portanto, se você quiser que a totalidade das bibliotecas principais também empregue PGO dinâmico, precisará definir. Em terceiro lugar, no entanto, o PGO dinâmico foi ensinado a instrumentar e otimizar adicionais.

O PGO já sabia instrumentar o despacho virtual. Agora no .NET 7, graças em grande parte ao dotnet/runtime#68703 , pode fazer isso para delegados (pelo menos para delegados a métodos de instância).

Eliminação de verificação de limites

Uma das coisas que torna o .NET atraente é sua segurança. O tempo de execução protege o acesso a arrays, strings e spans de forma que você não possa corromper a memória acidentalmente ao sair de uma das extremidades. Se você fizer isso, em vez de ler/escrever memória arbitrária, você obterá exceções através do JIT, inserindo verificações de limites toda vez que uma dessas estruturas de dados é indexada.

Sem verificações de limites, o que é mais facilmente visto pela falta do indicador call no final do método. Com esta melhoria, o JIT é capaz de entender o impacto de certas operações de multiplicação e deslocamento e suas relações com os limites da estrutura de dados.

Com essa melhoria, é possível visualizar:

  • O comprimento do array de resultado
  • O loop que está iterando de 0 até aquele limite superior exclusivo

Foi otimizado igualmente a eliminação de verificações de limites ao lidar com strings constantes e spans inicializados de métodos estáticos.

Regex

Antes do .NET 5, o Regex do .NET estava praticamente intocado. No .NET 5, foi atualizado para estar no mesmo nível ou melhor do que várias outras implementações do setor do ponto de vista do desempenho. O .NET 7 dá alguns saltos significativos a partir disso.

Abaixo um exemplo de aplicação Regex que exibe a seguinte saída:

Duplica ‘This’ na posição 0 e duplica ‘a’ na posição 66


using System;
using System.Text.RegularExpressions;

public class Class1
{
public static void Main()
{
string pattern = @"\b(\w+?)\s\1\b";
string input = "This this is a nice day. What about this? This tastes good. I saw a a dog.";
foreach (Match match in Regex.Matches(input, pattern, RegexOptions.IgnoreCase))
Console.WriteLine("{0} (duplicates '{1}') at position {2}",
match.Value, match.Groups[1].Value, match.Index);
}
}
// The example displays the following output:
// This this (duplicates 'This') at position 0
// a a (duplicates 'a') at position 66

Um dos novos recursos maiores do Regex, foi a nova implementação. Na atualização atual é possível alternar o processamento para o uso de um novo mecanismo baseado em autômatos finitos.

Ele tem dois modos principais de execução, um que depende de DFAs (autômatos finitos determinísticos) e outro que depende de NFAs (autômatos finitos não determinísticos). Ambas as implementações fornecem uma garantia muito valiosa: o tempo de processamento é linear no comprimento da entrada. Considerando que um mecanismo de retrocesso (que é o que usa se não for especificado) pode atingir uma situação conhecida como “retrocesso catastrófico”, onde expressões complexas combinadas com entradas problemáticas podem resultar em processamento exponencial no comprimento da entrada.

O Regex agora só fará uma quantidade de trabalho constante amortizada por caractere na entrada. No caso de um DFA, essa constante é muito pequena. Já em um NFA, essa constante pode ser muito maior, com base na complexidade do padrão. De todo modo, para qualquer padrão o trabalho ainda é linear no comprimento da entrada.

Essa implementação é baseada na noção de derivadas de expressões regulares, um conceito que existe há décadas (o termo foi originalmente cunhado em um artigo de Janusz Brzozowski na década de 1960) e que foi significativamente avançado para essa implementação. As derivadas de Regex formam a base de como os autômatos usados ​​para processar a entrada são construídos.

A ideia em seu núcleo é bastante simples: pegue um Regex e processe um único caractere… qual é o novo Regex que você obtém para descrever o que resta após o processamento desse caractere? Essa é a derivada. Por exemplo, dado o Regex para corresponder a três caracteres de palavra, se você aplicar isso ao próximo caractere de entrada ‘a’, isso removerá o primeiro, deixando-nos com a.

Novas APIs

As novas APIs foram habilitadas principalmente porque suportam entradas nos mecanismos Regex .

Na atualização atual, foi disponibilizado Regex para a era baseada em span do .NET, superando uma limitação significativa Regex desde que os spans foram introduzidos no .NET Core 2.1. Regex tem sido historicamente baseado no processamento de entradas e esse fato permeia o design e a implementação, incluindo as APIs expostas para o modelo de extensibilidade usado no .NET Framework ( agora está obsoleto e nunca foi funcional no .NET Core).

Uma sutileza que depende da natureza da entrada é como as informações de correspondência são retornadas aos chamadores. Retorna um objeto que representa a primeira correspondência na entrada e esse objeto expõe um método que permite mover para a próxima correspondência. Isso significa que o objeto precisa armazenar uma referência à entrada para que possa ser realimentado no mecanismo correspondente como parte de tal chamada.

Todos os mecanismos Regex dependem de uma classe base que armazena nele todo o estado necessário para alimentar os métodos e que compõem a lógica de correspondência real para as expressões regulares (esses métodos contêm todo o código principal para realizar a correspondência).

Uma otimização ocorreu para pular posições de entrada anteriores que não poderiam iniciar uma correspondência.

Foi adicionado um novo método virtual chamado Scanque. Este não precisa mais acessar os membros protegidos. Agora com o método ativo é possível apenas receber o span, já fatiado para a região de entrada e, consequentemente são processadas as entradas.

Uma das coisas legais sobre isso, do ponto de vista do desempenho, é que permite que o JIT faça um trabalho melhor ao reduzir várias despesas gerais, principalmente em torno da verificação de limites. Quando a lógica é implementada em termos de string de entrada, o mecanismo também recebe o início e o fim da região da entrada a ser processada (já que o desenvolvedor poderia ter chamado um método para processar apenas uma substring). Obviamente, a lógica de correspondência do mecanismo é muito mais complicada do que isso, mas, simplificando, imagine que a totalidade do mecanismo fosse apenas um loop sobre a entrada.

Loops e retrocessos

A manipulação de loops foi significativamente aprimorada, tanto no que diz respeito a processá-los mais rapidamente quanto no que diz respeito ao menor retrocesso.

Com os loops há duas principais melhorias:

  • Rapidez para consumir todos os elementos que correspondem ao loop
  • Rapidez para devolver elementos que podem ser necessários como parte do retrocesso para o restante da expressão para corresponder

Com loops lentos, foi levado em conta principalmente o retrocesso, que é a direção para frente (haja vista o fato de que os loops consomem como parte do retrocesso em vez de devolver como parte do retrocesso). Agora tanto no compilador quanto no gerador de código-fonte é feito o uso de todas as variantes para acelerar essas buscas.

Por exemplo, em um loop simples, a direção para frente desse loop implica consumir todos os caracteres até a próxima linha. Então, como parte do retrocesso, em vez de desistir de um caractere de cada vez, é possível encontrar o próximo local viável que possa corresponder ao restante do padrão.

Referência

Devblogs.microsoft.com/performance_improvements_in_net_7/

Compartilhe

Subscribe
Notify of
guest

0 Comentários
Mais velho
Novos Mais votados
Inline Feedbacks
View all comments

Subscribe to our Newsletter

Receive tips on technology, innovation, and other inspirations.

0
Would love your thoughts, please comment.x