O que está por trás da performance do .NET? Entenda definitivamente sobre Ahead of Time, Ready to Run, Just in Time Compilation, PGO, Tiered Compilation entre outros.
O .NET é uma plataforma que permite criar e executar aplicativos para diversas plataformas, como Windows, Linux, macOS, Android, iOS e WebAssembly. Para isso, o .NET usa um modelo de compilação híbrido, que combina diferentes técnicas de compilação para gerar código nativo ou intermediário que pode ser executado por uma máquina virtual ou diretamente pelo sistema operacional.
Para entender o que está por trás da performance do .NET precisamos explorar as principais tecnologias de compilação que o .NET usa e como elas impactam o desempenho dos aplicativos. Vamos ver o que são:
- Native AOT (ahead of time)
- Tiered compilation
- Ready to Run
- Profile guided optimization (PGO)
- Just In Time JIT
- RyuJIT
- Crossgen
- Crossgen2
O artigo do Sthephan Toube – Performance Improvements in .NET 8 – aborda todos os temas acima de forma detalhada. É altamente recomendável para programadores .NET a leitura.
O que é Native AOT (Ahead of Time)
O Native AOT é uma tecnologia avançada no ecossistema do .NET, que transforma o código dos aplicativos diretamente em código nativo durante o processo de publicação, em vez de depender de compilação no momento da execução. Esta abordagem elimina a necessidade de uma máquina virtual ou de um compilador just-in-time (JIT) durante a execução do aplicativo, trazendo melhorias notáveis no tempo de inicialização e no uso de memória.
Aplicações compiladas com Native AOT são particularmente eficazes em ambientes restritos, onde a execução de um JIT não é permitida. Este método de compilação tem estado presente no .NET desde a versão 3.1, o que demonstra a maturidade e a evolução contínua da plataforma.
Atualmente, o .NET utiliza o ReadyToRun em cenários de cliente e servidor e o Mono AOT para cenários móveis e WebAssembly. O Native AOT representa um passo adiante, trazendo a pré-compilação nativa completa para os cenários de cliente e servidor do .NET, expandindo as possibilidades de desempenho e eficiência em diferentes contextos de aplicação.
O que é JIT (Just In Time)
O Just In Time (JIT) é uma tecnologia central do .NET, responsável por compilar o código de um aplicativo em código nativo no momento da execução, ao invés de fazê-lo durante a fase de publicação. Essa abordagem oferece uma grande vantagem: permite que o aplicativo se ajuste de forma otimizada ao ambiente específico em que está sendo executado, levando em conta a arquitetura do sistema e o sistema operacional em uso.
Além de adaptar-se ao ambiente de execução, o JIT possibilita a utilização de recursos dinâmicos, tais como reflexão e geração de código em tempo real. Esta flexibilidade é um das grandes vantagens do .NET, permitindo que os aplicativos sejam mais responsivos e eficientes em diferentes cenários.
Desde o início do .NET, o JIT tem sido a tecnologia de compilação padrão da plataforma. O compilador JIT conhecido como RyuJIT desempenha um papel importante neste processo, convertendo o código intermediário (IL) em código nativo no momento em que o aplicativo é executado. Esta conversão just-in-time é o que confere ao .NET sua capacidade de gerar código altamente otimizado e personalizado para cada ambiente de execução, garantindo assim desempenho e eficiência.
Qual é a diferença entre compilação AOT e JIT?
A compilação Ahead Of Time (AOT) é uma técnica que transforma código intermediário (IL) em código nativo durante a fase de publicação do aplicativo. A grande vantagem da AOT reside na melhoria significativa do tempo de inicialização e na redução do consumo de memória dos aplicativos. No entanto, essa eficiência vem com certas limitações: o código gerado pode ser menos otimizado em termos de desempenho e menos portátil entre diferentes plataformas e arquiteturas. Além disso, o processo de compilação AOT pode exigir mais recursos durante a publicação, tanto em termos de tempo quanto de espaço.
Em contraste, a compilação Just In Time (JIT) gera código nativo a partir do IL no momento da execução do aplicativo. Essa abordagem permite uma adaptação mais fina ao ambiente específico de execução, gerando código que pode ser mais otimizado para o desempenho e mais portátil, considerando a diversidade de sistemas operacionais e arquiteturas de hardware. Um benefício adicional da compilação JIT é a menor demanda de recursos durante a fase de publicação, tanto em termos de tempo quanto de espaço.
O que é RyuJIT
RyuJIT representa a evolução dos compiladores Just In Time (JIT) no ecossistema do .NET. Como o compilador JIT padrão do .NET, o RyuJIT se destaca pela sua capacidade de gerar código nativo de alta qualidade e desempenho notável. Ele é compatível com uma ampla gama de arquiteturas, incluindo x86, x64, ARM e ARM64, abrangendo assim uma vasta gama de dispositivos e sistemas operacionais.
Um dos aspectos-chave do RyuJIT é seu design modular, que permite não apenas a reutilização eficiente de componentes comuns, mas também a personalização específica para cada arquitetura. Essa abordagem modular garante que o RyuJIT possa ser adaptado e otimizado para diferentes plataformas, mantendo a consistência e eficiência em seu desempenho de compilação.
RyuJIT está presente desde as primeiras versões do .NET Core, disponível a partir do .NET Core 1.0 para todas as arquiteturas suportadas. Para usuários do .NET Framework, o RyuJIT entrou em cena a partir da versão 4.6, inicialmente oferecendo suporte à arquitetura x64. Essa inclusão significou um avanço considerável na performance de aplicações .NET, contribuindo para a evolução contínua da plataforma em termos de eficiência e velocidade de execução.
O que é Tiered Compilation
A Tiered Compilation é uma inovação essencial no .NET para otimização do desempenho dos aplicativos. Essa técnica emprega dois níveis distintos de compilação, cada um com um foco específico: aceleração do início do aplicativo e otimização do desempenho em tempo de execução.
No primeiro nível, a compilação é realizada rapidamente, conhecida como quick JIT, ou então recorre-se ao código pré-compilado, disponível através do ReadyToRun. Esta fase prioriza a velocidade, garantindo que os aplicativos iniciem de forma mais ágil. Em seguida, no segundo nível, ocorre a compilação otimizada em segundo plano, referida como optimizing JIT. Este processo aprofunda-se na análise do código, buscando otimizações mais refinadas para melhorar o desempenho contínuo do aplicativo.
A Tiered Compilation mostrou-se tão eficaz que foi habilitada por padrão a partir do .NET Core 3.0. Para os desenvolvedores que necessitam de configurações personalizadas, é possível ajustar o comportamento da Tiered Compilation por meio da configuração System.Runtime.TieredCompilation
. Esta flexibilidade permite que os desenvolvedores alinhem a compilação às necessidades específicas de seus aplicativos, equilibrando entre inicialização rápida e desempenho otimizado.
O que é R2R (Ready to Run)
O Ready to Run (R2R) é uma tecnologia avançada no .NET que transforma a maneira como os aplicativos são compilados e executados, enfocando na eficiência e no desempenho. Esta tecnologia compila o código dos aplicativos em código nativo no momento da publicação, em vez de realizar essa compilação no momento da execução. O resultado é uma melhoria significativa no tempo de inicialização e na utilização de memória dos aplicativos.
Um aspecto interessante do Ready to Run é sua capacidade de permitir que os aplicativos sejam executados em máquinas que não possuem o runtime do .NET instalado. Isso representa um avanço considerável, principalmente para cargas de trabalho com um grande número de instâncias implantadas, como aquelas em ambientes de nuvem e infraestruturas de larga escala.
Apesar de usar um compilador ahead-of-time para converter o IL (Intermediate Language) em código nativo no momento da publicação, o Ready to Run não elimina a necessidade de um JIT (Just In Time Compiler) em todos os casos. Alguns métodos podem não ser pré-compilados, ou podem ser recompilados durante a execução para otimização ou correção. Esta abordagem híbrida garante que os aplicativos não só iniciem mais rapidamente, mas também mantenham um alto desempenho durante a execução.
Disponível a partir do .NET Core 3.0, o Ready to Run pode ser facilmente habilitado utilizando a opção --self-contained
ou --no-self-contained
ao publicar o aplicativo com o comando dotnet publish
. Esta flexibilidade permite que os desenvolvedores ajustem a compilação de acordo com as necessidades específicas de seus aplicativos, equilibrando eficientemente a inicialização e o desempenho.
O que é PGO (Profile Guided Optimization)
A profile guided optimization (PGO) é uma tecnologia de ponta no .NET que revoluciona a maneira como o código dos aplicativos é otimizado. Utilizando dados coletados durante a execução dos aplicativos, o PGO aprimora o desempenho, organizando melhor o código, otimizando o layout do cache, e fazendo uma alocação mais eficiente dos registros.
Disponível a partir do .NET 6, o PGO apresenta dois modos distintos de operação: estático e dinâmico. Cada um desses modos traz uma abordagem única para a coleta e utilização de dados de perfil, permitindo que os desenvolvedores escolham a melhor estratégia de acordo com as necessidades específicas de seus aplicativos.
PGO em Modo estático
No modo estático, o PGO utiliza dados de perfil coletados previamente para otimizar o código no momento da publicação do aplicativo. Este modo requer que o aplicativo seja executado com a opção DOTNET_TieredPGO=1
, gerando um arquivo .mibc
que contém os dados de perfil necessários. Após a coleta desses dados, o comando dotnet publish
deve ser utilizado com a opção -p:PublishReadyToRunUseProfileGuidedOptimization=<path-to-mibc-file>
para aplicar as otimizações durante a publicação.
PGO em Modo dinâmico
O modo dinâmico, por outro lado, coleta dados de perfil em tempo real, enquanto o aplicativo está sendo executado. Esta abordagem permite que o código seja otimizado dinamicamente, ajustando-se às condições atuais de execução. Para ativar o modo dinâmico, basta definir a variável de ambiente DOTNET_TieredPGO=1
ao executar o aplicativo. Não são necessárias etapas adicionais de publicação, simplificando o processo de otimização.
O que é Crossgen
A ferramenta Crossgen desempenha um papel importante na compilação de aplicativos, oferecendo uma abordagem única para a geração de código nativo. Ao contrário do Ready to Run, que se concentra na geração de código nativo neutro em termos de plataforma, o Crossgen se destaca por criar código específico para a arquitetura e o sistema operacional de destino. Esta especificidade permite que o código gerado seja altamente otimizado para o ambiente em que será executado.
Principais características do Crossgen
- Ao contrário do Ready to Run, que gera código que pode ser executado em diferentes plataformas, o Crossgen se concentra em criar código otimizado para um ambiente específico, maximizando assim o desempenho.
- Enquanto o Ready to Run se limita a compilar apenas as dependências elegíveis para pré-compilação, o Crossgen vai além, compilando todas as dependências do aplicativo, garantindo uma cobertura mais abrangente.
Como utilizar o Crossgen
Disponível desde o .NET Core 2.0, o Crossgen pode ser facilmente integrado ao processo de desenvolvimento. Para utilizá-lo, é necessário instalar o pacote Microsoft.NETCore.App.Runtime.<RID>
e executar o comando crossgen
. Este processo simples permite aos desenvolvedores otimizar seus aplicativos para desempenho máximo no ambiente de destino.
O impacto do Crossgen no desenvolvimento .NET
Ao escolher o Crossgen, os desenvolvedores do .NET podem garantir que seus aplicativos sejam compilados de maneira otimizada para a arquitetura específica em que serão executados. Esta abordagem pode resultar em ganhos significativos de desempenho, especialmente em ambientes onde a especificidade da arquitetura e do sistema operacional é uma consideração crítica.
O que é Crossgen2
Crossgen2 surge como uma evolução significativa do Crossgen original no ecossistema .NET, trazendo uma série de melhorias e novos recursos. Compartilhando o mesmo compilador que o RyuJIT, o Crossgen2 não só mantém a compatibilidade com as tecnologias existentes, mas também oferece uma experiência de compilação mais eficiente e robusta.
Principais características do Crossgen2
- Crossgen2 é projetado para ser mais rápido que seu predecessor, otimizando o tempo de compilação e melhorando a eficiência geral do processo de desenvolvimento.
- Uma das metas principais do Crossgen2 é oferecer uma plataforma mais estável e confiável para os desenvolvedores, reduzindo a incidência de erros e problemas durante a compilação.
- Com uma estrutura mais moderna e integrada, o Crossgen2 facilita a manutenção e atualizações futuras, garantindo que a ferramenta permaneça relevante e eficaz ao longo do tempo.
- Crossgen2 introduz suporte para tecnologias emergentes, como o composite R2R (Ready to Run) e PGO (Profile Guided Optimization), expandindo as capacidades de otimização de desempenho dos aplicativos.
Como utilizar o Crossgen2
Disponível a partir do .NET 6, o Crossgen2 pode ser facilmente incorporado ao fluxo de trabalho de desenvolvimento. Para ativá-lo, basta usar a opção -p:PublishReadyToRun=true
ao publicar o aplicativo com o comando dotnet publish
. Esta abordagem simplificada permite que os desenvolvedores tirem proveito das melhorias do Crossgen2 sem complicações adicionais.
O impacto do Crossgen2 no desenvolvimento em .NET
O advento do Crossgen2 representa um salto significativo em termos de desempenho de compilação, confiabilidade e suporte a novas funcionalidades no .NET. Ao optar pelo Crossgen2, os desenvolvedores podem assegurar que seus aplicativos se beneficiem das últimas inovações em compilação nativa, mantendo a compatibilidade com as tecnologias existentes e preparando-se para as futuras.
Recomendação
O artigo do Angelo Belchior é uma excelente fonte sobre as inovações do .NET 8, especialmente no que tange às tecnologias de compilação. De forma didática, ele aborda as diferenças entre as compilações JIT (just-in-time) e AOT (ahead-of-time), analisando como influenciam o desempenho, a portabilidade e a interoperabilidade dos aplicativos .NET. Com exemplos práticos e gráficos, Angelo demonstra as melhorias de desempenho introduzidas pelo .NET 8, destacando tanto as vantagens quanto as desvantagens de cada abordagem de compilação e oferecendo dicas valiosas para a escolha da tecnologia mais adequada em diferentes cenários.
Além disso, o artigo ressalta os desafios e limitações atuais na implementação mais ampla do AOT nativo, buscando compatibilidade com todos os recursos do .NET.
Recomendo fortemente a leitura deste artigo para desenvolvedores que desejam se manter atualizados sobre o .NET 8 e suas tecnologias de compilação. O artigo está disponível neste link: .NET 8, JIT e AOT. Parabéns ao Angelo Belchior pelo seu trabalho notável!
Conclusão
O universo do .NET é vasto e repleto de tecnologias de compilação inovadoras que têm um impacto significativo no desempenho dos aplicativos. Exploramos detalhadamente tecnologias como Native AOT, Tiered Compilation, Ready to Run, Profile Guided Optimization, Just In Time (JIT), RyuJIT, Crossgen e Crossgen2. Cada uma dessas tecnologias possui características únicas, vantagens e desvantagens específicas, e requer abordagens diferentes para sua habilitação e configuração.
Entender essas tecnologias é importante para os desenvolvedores que buscam otimizar seus aplicativos .NET, seja em termos de tempo de inicialização, consumo de memória ou desempenho geral. Com o conhecimento adquirido, é possível fazer escolhas informadas sobre qual tecnologia de compilação é mais adequada para cada cenário de aplicação, permitindo assim maximizar a eficiência e eficácia dos projetos desenvolvidos em .NET.
FAQ: Perguntas Frequentes
1. O que é o Intermediate Language (IL)?
O Código Intermediário (IL), também conhecido como Intermediate Language, é uma forma de linguagem de programação de baixo nível usada no ecossistema .NET. Quando um aplicativo é escrito em linguagens como C# ou VB.NET, o compilador dessas linguagens converte o código-fonte em IL, que é independente da plataforma e da arquitetura do hardware. Esse código IL é então encapsulado em arquivos, conhecidos como assemblies, que normalmente têm extensões .DLL ou .EXE.
A principal vantagem do IL é que ele permite que um único conjunto de código seja executado em diferentes plataformas e arquiteturas. Durante a execução ou a publicação do aplicativo, o IL é compilado em código nativo específico para a plataforma e a arquitetura do sistema pelo compilador Just-In-Time (JIT) ou pelo compilador Ahead-Of-Time (AOT). Essa compilação é realizada no momento da execução do aplicativo (no caso do JIT) ou no momento da publicação (no caso do AOT), garantindo assim que o aplicativo seja otimizado para o ambiente em que está sendo executado.
2. O que é o código nativo?
Código nativo refere-se a instruções de máquina que são diretamente executadas pelo processador de um computador. Este tipo de código é específico para a arquitetura do processador (como x86, x64, ARM) e para o sistema operacional em que o aplicativo está rodando. Diferentemente do código intermediário (IL), que é independente da plataforma, o código nativo é otimizado para o hardware específico, permitindo um desempenho mais eficiente e rápido.
No contexto do .NET, o código nativo é gerado a partir do código IL (Intermediate Language) pelo compilador JIT (Just-In-Time) durante a execução do aplicativo ou pelo compilador AOT (Ahead-Of-Time) no momento da publicação do aplicativo. Este processo de conversão assegura que o código do aplicativo seja transformado em instruções que podem ser diretamente entendidas e executadas pelo processador.
Os arquivos contendo código nativo são geralmente armazenados em formatos de arquivo como DLL (Dynamic Link Library) ou EXE (Executable), conhecidos como assemblies nativos. Estes assemblies contêm o código que já passou pelo processo de compilação específico para uma dada plataforma, pronto para ser executado de forma eficiente no hardware alvo.
3. Como escolher a melhor tecnologia de compilação para o meu aplicativo?
A escolha da melhor tecnologia de compilação depende dos requisitos e das características do seu aplicativo. Você deve considerar fatores como o tempo de inicialização, o desempenho, a portabilidade, a interoperabilidade, a segurança e o tamanho do aplicativo. Você também deve testar o seu aplicativo com diferentes tecnologias de compilação e medir os resultados.
De forma geral, você pode seguir as seguintes recomendações:
- Se o seu aplicativo precisa de um tempo de inicialização muito rápido e uma pegada de memória muito baixa, você pode usar o Native AOT.
- Se o seu aplicativo precisa de um bom equilíbrio entre o tempo de inicialização e o desempenho, você pode usar o Ready to Run ou o Crossgen2.
- Se o seu aplicativo precisa de um desempenho muito alto e uma portabilidade muito alta, você pode usar o JIT com o Tiered compilation e o PGO.
- Se o seu aplicativo precisa de recursos dinâmicos, como reflexão e geração de código, você deve usar o JIT.