Conteúdo

Just in Time: Como identificar problemas de performance e gargalos em aplicações em .NET?

Compreender o que está por trás da plataforma .NET, como o Just in Time (JIT), ajuda a melhorar a performance de aplicações desenvolvidas em .NET. Portanto, ter uma compreensão mais profunda sobre a métrica Just in Time Method Compiled fornecerá uma percepção clara sobre o desempenho do seu software.

Importante: O objetivo deste post é fornecer informações sobre como analisar problemas de desempenho relacionados à compilação do JIT no contexto do desenvolvimento de software. Embora o texto contenha uma breve explicação sobre o funcionamento do JIT, o foco principal não é aprofundar-se nesse aspecto técnico específico. Caso deseje obter um entendimento mais completo sobre o funcionamento do JIT, recomendo acessar esta página.

O que é Just in Time Compilation

O Just in Time Compilation, é uma técnica de compilação dinâmica projetada para otimizar a execução de código durante o tempo de execução. Em outras palavras, o JIT compila o código sob demanda, conforme o programa é executado.

Quando um programa .NET é executado, o código-fonte é inicialmente compilado para uma linguagem intermediária chamada Common Intermediate Language (CIL). A Common Intermediate Language (CIL), também conhecida como Intermediate Language (IL), é uma linguagem de programação de baixo nível usada no ambiente de programação da Microsoft. No .NET, o código de nível mais alto é compilado para CIL, que é então montado em bytecode, um código intermediário executado pela máquina virtual do Common Language Runtime (CLR). Esse bytecode é interpretado ou compilado para código de máquina nativo durante a execução, permitindo que os programas .NET sejam executados em diferentes plataformas e sistemas operacionais.

RyuJIT is the code name for the next generation Just in Time Compiler (aka “JIT”) for the AMD64 .NET runtime. Its first implementation is for the AMD64 architecture. It is derived from a code base that is still in use for the other targets of .NET.

The primary design considerations for RyuJIT are to:

  • Maintain a high compatibility bar with previous JITs, especially those for x86 (jit32) and x64 (jit64).
  • Support and enable good runtime performance through code optimizations, register allocation, and code generation.
  • Ensure good throughput via largely linear-order optimizations and transformations, along with limitations on tracked variables for analyses (such as dataflow) that are inherently super-linear.
  • Ensure that the JIT architecture is designed to support a range of targets and scenarios.

The first objective was the primary motivation for evolving the existing code base, rather than starting from scratch or departing more drastically from the existing IR and architecture.

O que é otimização adaptativa?

A otimização adaptativa feita pelo JIT é uma forma de ajustar e otimizar o código durante o tempo de execução, com base nas informações coletadas sobre o comportamento do programa. Essa abordagem visa melhorar o desempenho e a eficiência do código gerado, adaptando-o às condições reais de uso.

Baseada na premissa de que o comportamento de um programa pode variar ao longo do tempo, o JIT coleta informações durante a execução, como frequência de chamada de métodos, padrões de uso de variáveis e caminhos de execução mais comuns. Com base nesses dados, o compilador JIT pode fazer ajustes no código gerado, aplicando otimizações específicas que são mais relevantes para o cenário atual de execução.

Nesse exemplo, temos uma função CalculateSum que calcula a soma de um array de números. A função é chamada no método Main, onde criamos um array com 1 milhão de elementos e, em seguida, calculamos a soma.

O problema de desempenho aqui está relacionado ao JIT Compiler. Quando executamos esse código, o JIT Compiler precisa compilar o método CalculateSum na primeira chamada. Se essa compilação ocorrer repetidamente durante a execução do programa, isso pode afetar o desempenho.

Para mitigar esse problema, podemos pré-compilar o método CalculateSum usando o atributo MethodImplOptions.AggressiveInlining. Isso instrui o compilador a incluir o código diretamente no local de chamada, eliminando a necessidade de compilação JIT repetida. Aqui está o código modificado:

Ao usar o atributo MethodImplOptions.AggressiveInlining no método CalculateSum, estamos instruindo o compilador a incorporar o código diretamente no local de chamada. Isso evita a compilação JIT repetida, o que pode melhorar o desempenho em cenários em que a função é chamada várias vezes.

Vale ressaltar que o uso do atributo MethodImplOptions.AggressiveInlining pode não ser adequado em todos os casos e é importante fazer testes e medições para verificar se há realmente benefícios de desempenho significativos. Em alguns casos, a compilação JIT pode ser benéfica para otimizações específicas do hardware e do ambiente de execução.

Quais os benefícios do JIT?

O primeiro benefício notável do JIT é o desempenho otimizado que ele proporciona. Ao compilar o código CIL em tempo de execução e convertê-lo em código de máquina nativo específico da plataforma, o JIT permite que o código seja executado de maneira mais eficiente. Além disso, o JIT aplica otimizações adaptativas com base no contexto de execução, como inlining (incorporação de código) e eliminação de código morto, maximizando a eficiência do código gerado.

Outro benefício é a interoperabilidade. Ele gera código de máquina nativo adequado para a plataforma em que o programa está sendo executado, possibilitando que aplicações .NET sejam executadas em diferentes arquiteturas de processadores e sistemas operacionais. Isso garante a portabilidade do código e facilita a interoperabilidade entre plataformas, permitindo que os desenvolvedores alcancem um público mais amplo.

Embora a compilação JIT inicial possa ter um impacto no tempo de inicialização do programa, esse trade-off é compensado pelo desempenho aprimorado em tempo de execução. O JIT permite um equilíbrio eficiente entre o tempo de inicialização e o desempenho geral da aplicação, garantindo uma experiência positiva para os usuários.

Como descobrir problemas de performance em uma aplicação .NET causados por compilações e tempo de compilação altos?

Investigar e identificar problemas de desempenho em uma aplicação .NET pode garantir um bom funcionamento e uma experiência positiva para os usuários. Dois aspectos que podem impactar significativamente o desempenho são o alto número de métodos compilados e o tempo de compilação.

O tempo de inicialização pode ser prejudicado devido o número alto de compilações realizadas pelo JIT.

A inicialização de uma aplicação .NET pode ser afetada por um tempo mais lento devido ao alto número de compilações realizadas pelo JIT. Esse processo de compilação sob demanda pode impactar o tempo necessário para a aplicação estar totalmente pronta para uso. Durante a inicialização, o JIT compila o código CIL em código de máquina nativo, o que pode consumir tempo e recursos. O número de compilações necessárias e o tempo que cada compilação leva dependem da complexidade do código e da arquitetura da aplicação.

No Prometheus, existem duas métricas que podem auxiliar na investigação de problemas de desempenho causados pelo JIT. As métricas process_runtime_dotnet_jit_methods_compiled_count e process_runtime_dotnet_jit_compilation_time_ns fornecem informações valiosas sobre o comportamento do JIT e permitem uma análise mais aprofundada das possíveis causas de baixo desempenho em uma aplicação .NET.

A captura de tela acima ilustra o processo de inicialização de uma API desenvolvida em .NET 7. Ao analisar a imagem, podemos observar que, durante a primeira execução da API, o tempo de inicialização foi de 50ms, com 30 métodos compilados. Nesse momento, o tempo de resposta da API era de 1 segundo. Além disso, é possível perceber o comportamento adaptativo, em que o tempo de resposta oscilava. Após a conclusão da compilação, o tempo de resposta se estabilizou e as compilações deixaram de ocorrer. Também podemos observar que ao iniciar a quantidade de requisições é muito pequena, pois o sistema está ocupado fazendo as compilações.

Portanto, como o JIT é adaptativo, ele pode ser acionado a qualquer momento, resultando em novas compilações e potencialmente aumentando o tempo de resposta. É crucial monitorar essa métrica de forma contínua. Se o número e o tempo de compilação forem elevados, isso pode ter um impacto significativo no tempo de resposta da aplicação e no throughput. É essencial estar atento a esses indicadores para identificar possíveis gargalos de desempenho e otimizar a aplicação, garantindo uma experiência aprimorada para os usuários.

Quais são os principais fatores que acionam o JIT e como podemos lidar com eles de forma eficiente?

Existem várias causas possíveis para o alto número de métodos compilados e o tempo de compilação prolongado. Algumas delas incluem:

  • Quando uma aplicação realiza um alto número de chamadas de métodos no início da execução, ocorre uma inicialização excessiva que pode resultar em uma grande quantidade de métodos compilados pelo JIT. Analise cuidadosamente se todas essas chamadas de métodos são necessárias e se há possibilidade de otimizar o processo de inicialização para reduzir a carga no JIT. Dessa forma, é possível minimizar o tempo de inicialização e otimizar o desempenho geral da aplicação.
  • A utilização excessiva de assemblies ou bibliotecas desnecessárias pode aumentar o número de métodos compilados. Revise o uso de assemblies, carregando apenas aqueles necessários para a funcionalidade da aplicação.
  • Se a aplicação utiliza recursos que geram código dinamicamente durante a execução, como reflexão ou geração de código em tempo de execução, como auto-mapper, isso pode levar a um maior número de métodos compilados. Nesses casos, é importante avaliar se o uso de código dinâmico é realmente necessário e se é possível reduzir a sua utilização para melhorar o desempenho.
  • O .NET utiliza um mecanismo de cache de compilação para evitar a recompilação desnecessária de métodos. Quando um método é chamado novamente, o JIT verifica se ele já foi compilado anteriormente e, se sim, reutiliza a compilação existente em vez de compilar novamente. Isso evita a sobrecarga de compilação repetida do mesmo método.

Conclusão

A compreensão do Just in Time Compilation no .NET Core e sua influência no desempenho das aplicações é essencial para os desenvolvedores. Ao analisar métricas como o número de métodos compilados e o tempo de compilação, é possível identificar possíveis gargalos e tomar ações corretivas para otimizar o desempenho. A configuração adequada do cache de compilação e a otimização adaptativa são estratégias importantes para reduzir o tempo de inicialização e melhorar a eficiência do código gerado pelo JIT. Ao se aprofundar nessas práticas, os desenvolvedores podem criar aplicações .NET mais eficientes, responsivas e escaláveis.

FAQ: Perguntas Frequentes

1. O que é Just in Time Compilation

O Just-in-Time Compilation é uma técnica de compilação dinâmica que otimiza a execução do código durante o tempo de execução. Ele converte o código intermediário (CIL) em código de máquina nativo específico da plataforma, permitindo uma execução mais eficiente.

2. Como identificar problemas de desempenho relacionados ao JIT?

Através do monitoramento de métricas como a contagem de métodos compilados e o tempo de compilação, é possível identificar se o JIT está afetando o desempenho de uma aplicação. Valores elevados nessas métricas podem indicar a necessidade de otimizações ou revisão do código.

3. Quais são as possíveis causas para um alto número de métodos compilados?

Um alto número de métodos compilados pode ser causado por uma inicialização excessiva da aplicação, uso desnecessário de assemblies ou bibliotecas, ou utilização de recursos que geram código dinamicamente durante a execução.

4. Como otimizar a inicialização de uma aplicação .NET?

Para otimizar a inicialização de uma aplicação .NET, é importante analisar e reduzir o número de chamadas de métodos desnecessárias, revisar o uso de assemblies e bibliotecas, e avaliar o uso de recursos que geram código dinamicamente durante a execução.

Compartilhe:

Tiago Tartari

Tiago Tartari

Eu ajudo e capacito pessoas e organizações a transformar problemas complexos em soluções práticas usando a tecnologia para atingir resultados extraordinários.

Qual é o desafio
que você tem hoje?