.NET 8 - AOT Compilation


  Hoje vou apresentar a compilação Ahead of Time (AOT) no .NET 8.

Um dos recursos previstos para o .NET 8 é a compilação Ahead of Time, e neste artigo, vou apresentar o que temos até o momento nas versões preview do .NET 8 SDK. (alguns recursos poderão ser alterados até o lançamento final do .NET 8)

A compilação Ahead of Time (AOT) é um processo de compilação de código fonte em linguagem de programação para um formato executável nativo antes da execução do programa. Ao contrário da compilação Just-in-Time (JIT), que compila o código durante a execução, a compilação AOT ocorre antes do tempo de execução, geralmente como parte do processo de construção ou implantação do software.

A compilação Ahead of Time (AOT) é um dos principais recursos que estão sendo trabalhados pela equipe ASP.NET da Microsoft para o .NET 8.

Seja qual for a linguagem que você usa na plataforma .NET (C#, F#, VB.NET etc), você usa um compilador para gerar os byte codes da linguagem intermediária (IL). Atualmente  executamos esta etapa usando o comando dotnet build no projeto para produzir um executável que contém o IL e toda uma carga de metadados.

A próxima etapa do processo é converter o código da IL para o assembly, o que é feito em um tempo de execução pelo runtime do .NET usando o compilador Just-in-Time (JIT).

Considerando a linguagem C# a compilação de um projeto básico envolveria as seguintes etapas:

  1. Pré-processamento: Nesta etapa, o pré-processador do C# é executado para processar as diretivas de pré-processador presentes no código fonte.
     
  2. Compilação: O compilador do C# (csc.exe) é usado para compilar os arquivos de código-fonte (.cs) em arquivos de objeto (.obj). O compilador gera o código intermediário na forma de CIL (Common Intermediate Language) ou IL (Intermediate Language).
     
  3. Compilação de CIL: O próximo passo é a compilação de CIL para código de máquina específico da plataforma de destino. Isso é feito pelo JIT (Just-in-Time) Compiler durante a execução do programa.
     
  4. Geração de Assembly: Nesta etapa, o compilador cria um ou mais arquivos de assembly (.dll ou .exe) a partir dos arquivos de objeto compilados.
     
  5. Referências de Assemblies: Se o projeto tiver referências a bibliotecas ou assemblies externos, esses assemblies serão referenciados e copiados para o diretório de saída do projeto durante a compilação.
     
  6. Empacotamento: Se o projeto estiver sendo empacotado como uma biblioteca ou um pacote NuGet, haverá uma etapa adicional de empacotamento.

Na compilação Ahead of Time (AOT)  na plataforma .NET, o código C# é compilado diretamente em código de máquina nativo, sem a etapa de geração de código intermediário CIL/IL. Isso permite melhor desempenho e tempo de inicialização mais rápido do programa, além de otimizações específicas da plataforma de destino.

Quais os motivos para usar a compilação AOT e quais as vantagens e desvantagens ?

Prós e Contras da compilação AOT

Como a compilação AOT gera instruções de código assembly, não há compilação JIT em tempo de execução quando o programa é executado. Isso oferece uma grande vantagem - a redução do tempo de inicialização.

Normalmente, antes do runtime do .NET executar um método, ele executa o compilador JIT no IL do método para gerar o código assembly a ser executado. Quando o aplicativo é inicializado, geralmente há muitas classes e métodos que precisam ser compilados pelo JIT. Isso tudo se soma e geralmente significa que os aplicativos .NET podem demorar um pouco para iniciar.

Em aplicativos de servidor tradicionais que são iniciados apenas uma vez e permanecem em execução por horas ou dias, demorar muito para iniciar realmente não importa. Mas o tempo de inicialização é importante para aplicativos que usam AWS Lambda ou Azure Functions. Esses aplicativos são ativados em resposta a uma solicitação, executados uma vez e encerrados. Para esses aplicativos, que podem cobrar por milissegundos, o tempo de inicialização é crucial.

Este cenário específico é onde a compilação AOT para .NET realmente brilharia. É por isso que é um foco para o .NET 8. Entretanto a compilação AOT não será sempre  "melhor" do que usar a compilação JIT. Ela apresenta muitas desvantagens, o que significa que o AOT geralmente não é sempre a escolha certa; a abordagem JIT pode ser melhor para o seu caso de uso.

Principais vantagens da compilação AOT:

  1. Desempenho de tempo de execução: A compilação AOT geralmente resulta em um desempenho de tempo de execução mais rápido, pois o código é traduzido antecipadamente para código de máquina nativo.
     
  2. Tempo de inicialização mais rápido: Como o código já está pré-compilado, não há necessidade de compilação adicional durante a execução. Isso resulta em um tempo de inicialização mais rápido do programa, o que é particularmente benéfico para aplicativos de inicialização rápida e cenários onde a resposta imediata é importante.
     
  3. Otimizações avançadas: A compilação AOT permite que o compilador execute otimizações mais avançadas, pois tem acesso completo ao código-fonte durante a compilação.

Principais desvantagens da compilação AOT:

  1. Tamanho do arquivo executável: A compilação AOT pode aumentar o tamanho do arquivo executável, pois o código de máquina nativo é incluído diretamente no arquivo. Isso pode resultar em um aumento no espaço em disco necessário para armazenar o programa e em um maior consumo de memória durante a execução.
     
  2. Portabilidade limitada: Como o código é compilado para a plataforma de destino específica antecipadamente, pode haver uma limitação na portabilidade do programa para diferentes plataformas.

Então em quais cenários seria recomendado usar a compilação Ahead of Time (AOT) ?

  1. Aplicativos de tempo de execução crítico: Se você tem um aplicativo em que o desempenho e a resposta imediata são fundamentais, como jogos, aplicativos de realidade virtual/aumentada ou sistemas em tempo real, a compilação AOT pode ser benéfica para garantir um desempenho rápido e consistente.
     
  2. Redução de tempo de inicialização: Se o tempo de inicialização rápido do aplicativo é uma prioridade, especialmente em aplicativos de inicialização rápida, aplicativos móveis ou serviços da web que precisam responder rapidamente às solicitações, a compilação AOT pode ajudar a reduzir o tempo de inicialização e melhorar a experiência do usuário.
     
  3. Otimização de desempenho avançada: Se você precisa aproveitar otimizações específicas da plataforma de destino e realizar análises avançadas de código-fonte durante a compilação, a compilação AOT pode permitir otimizações mais avançadas que não são possíveis com a compilação JIT.

Explorando a compilação AOT no .NET 8

No Visual Studio 2022 Preview no template de projeto Console App temos a opção para habilitar a compilação AOT :

Usando a ferramenta NET CLI no prompt de comandos podemos usar o template api : dotnet new api - para criar uma ASP.NET Core Web API que vai gerar uma minimal API onde agora podemos usar a opção --aot.

A opção -aot ajusta o código gerado da seguinte forma:

  1. Configura o gerador de fonte JSON .NET 6 e adiciona uma implementação JsonSerializerContext
  2. Define a propriedade MSBuild como PublishAot=true no arquivo .csproj.

Então vamos ver isso funcionando na prática...

Estou usando o .NET 8 SDK 8.0.1000-preview.5.23303.2 e vou criar uma api chamada demoaot emitindo o seguinte comando : dotnet new api -o demoaot --aot

Na versão final do .NET 8 o template usado para gerar uma minimal API com compilação AOT foi alterado para:

dotnet new webapiaot

Entrando na pasta demoaot temos os seguintes arquivos gerados :

Vamos visualizar o código do arquivo Program.cs usando o VS Code :

using System.Text.Json.Serialization;
using demoaot;

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options =>
{
   options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

var app = builder.Build();

var sampleTodos = TodoGenerator.GenerateTodos().ToArray();

var todosApi = app.MapGroup("/todos");

todosApi.MapGet("/", () => sampleTodos);
todosApi.MapGet("/{id}", (int id) =>
                             sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo
                            ? Results.Ok(todo)
                            : Results.NotFound());

app.Run();

[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}
 

Destaques do código gerado:

1- Note o uso do Slim builder (um novo recurso do .NET 8)


 var builder = WebApplication.CreateSlimBuilder(args);    

2- A configuração do JSON source generator

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

 

3- A geração de um array de objetos 'Todo' :

  var sampleTodos = TodoGenerator.GenerateTodos().ToArray();    

 

4- O contexto de serialização requerido para o source generation :

 [JsonSerializable(typeof(Todo[]))]
 internal partial class AppJsonSerializerContext : JsonSerializerContext
 {
 }

Vejamos agora o código do arquivo  demoaot.csproj :

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
      <TargetFramework>net8.0</TargetFramework>
      <Nullable>enable</Nullable>
      <ImplicitUsings>enable</ImplicitUsings>
      <ServerGarbageCollection>false</ServerGarbageCollection>
       <InvariantGlobalization>true</InvariantGlobalization>
        <PublishAot>true</PublishAot>
    </PropertyGroup>

</Project>

Destaques do código gerado:

1- Desabilita o Garbage Collector para reduzir o consumo de memória

 <ServerGarbageCollection>false</ServerGarbageCollection>

2- Uso da globalização invariant para reduzir o tamanho da aplicação

<InvariantGlobalization>true</InvariantGlobalization>

3- Habilita a publicação como AOT

<PublishAot>true</PublishAot>

Publicando a aplicação

Para publicar a aplicação usando a compilação AOT podemos executar o comando: dotnet publish

Observe que, para usar a compilação AOT, você precisa instalar a carga de trabalho "Desenvolvimento de desktop com C++" do Visual Studio 2022 (ou os pré-requisitos descritos aqui).

Sem fazer isso , você pode ver erros como: Linker de plataforma não encontrado ou erro fatal LNK1181: não é possível abrir o arquivo de entrada 'advapi32.lib'.

Após isso podemos entrar na pasta: D:\projetos\demoaot\bin\Release\net8.0\win-x64 e podemos executar o projeto : demoaot.exe

Agora vamos usar o Postman e acessar o endpoint da api em : http://localhost:5000/todos

Nossa aplicação web api compilada com a opção --aot esta respondendo e funcionando e o tamanho do arquivo demoaot.exe gerado é de apenas 157696 bytes.

E estamos conversados.

"Porque pela graça sois salvos, por meio da fé; e isto não vem de vós, é dom de Deus.
Não vem das obras, para que ninguém se glorie;"
Efésios 2:8,9

Referências:


José Carlos Macoratti