Esta página puede contener texto traducido automáticamente.

Rendimiento de C# Native AOT

¿Qué tan rápidas son las aplicaciones .NET Native AOT en comparación con el código administrado normal? ¿Puede AOT superar a JIT? ¿Cómo evaluar las aplicaciones Native AOT?

Comparación del rendimiento de Native AOT

Este artículo es parte de una serie sobre Native AOT en .NET. Si no está familiarizado con Native AOT, lea primero la parte Cómo desarrollar aplicaciones Native AOT en .NET.

Este artículo compara el rendimiento de .NET y Native AOT. Primero, revisaremos los benchmarks oficiales de Microsoft. Permiten comparar diferentes opciones de implementación de .NET para aplicaciones ASP.NET simples.

Luego, aprenderá a ejecutar sus propios puntos de referencia utilizando BenchmarkDotNet y las herramientas hyperfine. Estos puntos de referencia le permiten medir la velocidad del código en su entorno.

Puntos de referencia de ASP.NET

El equipo de ASP.NET mantiene una infraestructura sólida para las pruebas de rendimiento. Prueban varios escenarios en diferentes entornos.

Estamos muy interesados ​​en los puntos de referencia de Native AOT. La principal fuente de información es el siguiente panel de PowerBI. Los datos que se encuentran allí se basan en 3 "ballenas": aplicaciones de prueba, escenarios de implementación y métricas.

Aplicaciones de prueba

Puede encontrar el código fuente de los benchmarks y las aplicaciones de prueba en el repositorio aspnet/Benchmarks.

Los benchmarks de Native AOT comparan 3 tipos de aplicaciones:

  • Stage1: una API mínima basada en HTTP y JSON. El código fuente de la aplicación se encuentra en /src/BenchmarksApps/BasicMinimalApi.
  • Stage1Grpc: una API similar basada en gRPC (/src/BenchmarksApps/Grpc/BasicGrpc)
  • Stage2: aplicación web completa que incluye base de datos y autenticación (/src/BenchmarksApps/TodosApi)

Escenarios de implementación de .NET

Las aplicaciones de prueba se ejecutan en diferentes entornos. En este momento, los puntos de referencia utilizan máquinas virtuales Windows y Linux con 28 núcleos. También hay entornos Linux separados para procesadores ARM e Intel.

Las aplicaciones también se prueban en diferentes configuraciones. Una aplicación en alguna configuración define un "escenario".

Puede mantener presionada la tecla Ctrl (o ⌘) para seleccionar varios escenarios o entornos en el panel de PowerBI.

Métricas

Los puntos de referencia recopilan métricas fundamentales para cada aplicación implementada. Por ejemplo, las pruebas miden la cantidad de solicitudes por segundo (RPS), el tiempo de inicio y el conjunto de trabajo de memoria máxima.

Eso nos permite comparar los valores de las métricas para varias configuraciones de la misma aplicación.

Comparación de rendimiento

Compararemos los escenarios de StageX con StageXAot y StageXAotSpeedOpt. Utilizan la siguiente configuración:

Escenario Argumentos de compilación de dotnet publish
StageX PublishAot=false
EnableRequestDelegateGenerator=false
Stage2Aot PublishAot=true
StripSymbols=true
Stage2AotSpeedOpt PublishAot=true
StripSymbols=true
OptimizationPreference=Speed

Todos los escenarios anteriores también utilizan la variable de entorno DOTNET_GCDynamicAdaptationMode=1.

Los escenarios StageXAotSpeedOpt permiten estimar el impacto de la configuración OptimizationPreference = Speed.

También puede revisar los escenarios de StageXTrimR2RSingleFile. Dichos escenarios corresponden a la implementación ReadyToRun recortada, que es otra forma de compilación anticipada en .NET. A veces, es una buena alternativa a Native AOT.

Estos son los resultados de la comparación de rendimiento actual para .NET 9 Release Candidate (septiembre de 2024):

Tiempo de inicio

Las aplicaciones AOT se inician mucho más rápido que las versiones administradas. Esto es así tanto para las aplicaciones de Stage1 como de Stage2 y para todos los entornos. Resultados de muestra:

Escenario Tiempo de inicio (ms)
Stage2AotSpeedOpt 100
Stage2Aot 109
Stage2 528

Conjunto de trabajo

El conjunto de trabajo máximo para las aplicaciones Native AOT es menor que para las versiones administradas. En Linux, las versiones administradas usan aproximadamente entre 1,5 y 2 veces más RAM que las versiones AOT. Por ejemplo:

Escenario Conjunto de trabajo máximo (MB)
Stage1Aot 56
Stage1AotSpeedOpt 57
Stage1 126

En Windows, la diferencia es menor. Especialmente, para Stage2:

Escenario Conjunto de trabajo máximo (MB)
Stage2Aot 152
Stage2AotSpeedOpt 150
Stage2 167

Solicitudes por segundo

Los valores de RPS más altos significan una aplicación más rápida. La aplicación ligera Stage1 generalmente maneja alrededor de 800-900K solicitudes por segundo. La aplicación Stage2 más grande solo maneja alrededor de 200K solicitudes.

Para la aplicación Stage2, la versión .NET maneja más solicitudes que las versiones AOT en todos los entornos. La velocidad de la versión Stage2AotSpeedOpt a veces es similar, pero, por lo general, se encuentra entre Stage2 y Stage2Aot. Estos son los resultados típicos:

Escenario RPS
Stage2 235.008
Stage2AotSpeedOpt 215.637
Stage2Aot 194.264

Los resultados de la aplicación Stage1 son similares en Intel Linux e Intel Windows. Sin embargo, en Ampere Linux, AOT supera a la versión administrada. Resultados de muestra de Ampere Linux:

Escenario RPS
Stage1AotSpeedOpt 929.524
Stage1Aot 912.344
Stage1 844.659

Por lo tanto, el entorno y el código de la aplicación pueden afectar significativamente la velocidad. Tiene sentido ejecutar sus propios puntos de referencia para estimar los beneficios de Native AOT para su proyecto. Escribamos puntos de referencia personalizados sin la infraestructura de prueba de Microsoft.

Evaluación comparativa de aplicaciones Native AOT

Usaremos 2 tipos de benchmarks. El primero se basa en BenchmarkDotNet, la biblioteca popular para realizar benchmarks de código .NET. Estos benchmarks comparan la velocidad pura, excluyendo el tiempo de inicio.

El segundo se basa en la herramienta hyperfine. Permite comparar el tiempo de ejecución de dos comandos de shell. Estos puntos de referencia comparan la velocidad general, incluido el tiempo de inicio.

No compararemos el consumo de memoria aquí. En este momento, el diagnóstico NativeMemoryProfiler en BenchmarkDotNet no es compatible con el entorno de ejecución de Native AOT. Hyperfine tampoco realiza un seguimiento del uso de la memoria actualmente.

Puede descargar el código fuente del repositorio NativeAotBenchmarks en GitHub. Le recomendamos que los pruebe en su entorno. Este artículo describe los resultados de una computadora portátil con Windows 11 con procesador Intel Core i9-13900H y 16 Gb de RAM.

Asegúrese de ejecutar los benchmarks correctamente. Estas son las recomendaciones habituales:

  • Use la compilación Release.
  • Desactive todas las aplicaciones excepto el proceso de evaluación comparativa. Por ejemplo, deshabilite el software antivirus, cierre Visual Studio y un navegador web.
  • Mantenga su computadora portátil enchufada y use el mejor modo de rendimiento.
  • Utilice los mismos datos de entrada en los escenarios que se comparan.

Casos de prueba

Realizaremos pruebas comparativas en 2 escenarios en .NET 8:

1. Código C# simple para una compresión de cadena usando el conteo de caracteres repetidos. Por ejemplo, la cadena "aabcccccaaa" se convertiría en "a2b1c5a3":

string Compress(string s)
{
    StringBuilder compressed = new(s.Length);

    for (int i = 0; i < s.Length; ++i)
    {
        char c = s[i];
        for (int j = i + 1; j <= s.Length; ++j)
        {
            if (j == s.Length || s[j] != c)
            {
                compressed.Append(c + $"{j - i}");
                i = j - 1;

                if (compressed.Length > s.Length)
                    return s;

                break;
            }
        }
    }

    if (compressed.Length <= s.Length)
        return compressed.ToString();

    return s;
}

2. Una tarea más pesada conversión de PDF a PNG que utiliza Docotic.Pdf.

Requisitos previos

Instalar prerrequisitos para la implementación de .NET Native AOT.

Instalar hyperfine para ejecutar los benchmarks correspondientes.

Para realizar pruebas comparativas de PDF a PNG, obtenga una clave de licencia gratuita por tiempo limitado en la página Descargar la biblioteca PDF de C# .NET. Debe aplicar la clave de licencia en Helper.cs.

BenchmarkDotNet

Estos puntos de referencia se encuentran en el proyecto NativeAotBenchmarks. Comparamos los resultados de RuntimeMoniker.NativeAot80 y RuntimeMoniker.Net80. De forma predeterminada, BenchmarkDotNet crea código Native AOT con la configuración OptimizationPreference=Speed.

BenchmarkDotNet realiza 6 o más iteraciones de calentamiento. Esto ayuda a JIT a precompilar el código y recopilar algunas estadísticas. Por lo tanto, estos puntos de referencia excluyen el tiempo de inicio de la comparación.

Compresión de cadenas

El benchmark CompressString para la compresión de cadenas utiliza una cadena larga con caracteres duplicados. El error común sería generar una cadena aleatoria. En tal caso, los benchmarks para Native AOT y .NET 8 utilizarían cadenas de entrada diferentes. Es posible utilizar cadenas aleatorias, pero es necesario inicializar un generador aleatorio con la misma semilla.

La versión Native AOT se ejecuta aproximadamente 1,08 veces más rápido que la versión .NET 8:

Method Runtime Mean Error StdDev
Compress .NET 8.0 4.117 ms 0.0553 ms 0.0517 ms
Compress NativeAOT 8.0 3.809 ms 0.0403 ms 0.0377 ms

PDF a PNG

Los benchmarks de PDF a PNG procesan documentos PDF en la memoria, lo que permite excluir la interacción con el sistema de archivos. Las operaciones de E/S con un disco pueden distorsionar los resultados del benchmark.

Probamos la velocidad con dos documentos PDF. El primero, Banner Edulink One.pdf, es más complejo. Se convierte a un PNG de 72 ppp y requiere más tiempo para procesarse. La versión .NET 8 es un poco más rápida para este documento:

Method Runtime Mean Error StdDev
Convert .NET 8.0 1.103 s 0.0156 s 0.0146 s
Convert NativeAOT 8.0 1.167 s 0.0160 s 0.0149 s

El segundo documento es más pequeño y más simple. Se convierte a un PNG de 300 ppp. Y la velocidad es casi igual:

Method Runtime Mean Error StdDev
Convert .NET 8.0 290.1 ms 5.78 ms 6.88 ms
Convert NativeAOT 8.0 288.3 ms 4.44 ms 3.94 ms

hyperfine

Estos puntos de referencia se encuentran en el proyecto NativeAotTestApp. El proyecto no utiliza la configuración OptimizationPreference=Speed. Puede habilitarla en NativeAotTestApp.csproj: <OptimizationPreference>Speed</OptimizationPreference>

Utilice el script benchmark.bat para ejecutar pruebas en Windows. Puede convertirlo a Bash para sistemas operativos basados ​​en Unix/Linux. El script crea versiones .NET 8 y Native AOT de la misma aplicación. Luego, compara su rendimiento con comandos similares: hyperfine --warmup 3 "net8-app.exe" "native-aot-app.exe"

El calentamiento en modo hiperfino ayuda a iniciar aplicaciones de prueba en cachés de disco "calientes". A diferencia de BenchmarkDotNet, el calentamiento hiperfino no ayuda a JIT. Por lo tanto, los puntos de referencia hiperfinos comparan la velocidad total de la aplicación, incluido el tiempo de inicio.

Nuestra aplicación de prueba admite el argumento de recuento de iteraciones. Permite repetir el mismo código varias veces en un bucle simple:

for (int i = 0; i < iterationCount; ++i)
    CompressString(args);

La idea es reducir el impacto de la diferencia de tiempo de inicio. Repetir el mismo código le da a JIT la oportunidad de recopilar más estadísticas de tiempo de ejecución y generar código más rápido.

Una situación común es la siguiente. La primera vez, ejecutas pruebas comparativas con una sola iteración. Una versión Native AOT funciona mucho más rápido. Luego, ejecutas las mismas pruebas comparativas con múltiples iteraciones y la velocidad total de ambas versiones se vuelve igual. Esto significa que después del inicio, una versión administrada es realmente más rápida.

Compresión de cadenas

Para 100.000 iteraciones de la misma compresión de cadena de entrada, el rendimiento de Native AOT es mejor:

Benchmark 1: .NET 8 version (100000 iterations)
  Time (mean ± σ):     151.5 ms ±   2.6 ms    [User: 32.1 ms, System: 1.6 ms]
  Range (min … max):   148.0 ms … 157.5 ms    19 runs

Benchmark 2: Native AOT version (100000 iterations)
  Time (mean ± σ):      55.1 ms ±   3.1 ms    [User: 15.0 ms, System: 2.1 ms]
  Range (min … max):    51.6 ms …  65.9 ms    51 runs

Summary
  Native AOT version ran 2.75 ± 0.16 times faster than .NET 8 version

Pero la velocidad se vuelve casi la misma para 10.000.000 de iteraciones:

Benchmark 1: .NET 8 version (10000000 iterations)
  Time (mean ± σ):      3.984 s ±  0.139 s    [User: 2.946 s, System: 0.009 s]
  Range (min … max):    3.790 s …  4.182 s    10 runs

Benchmark 2: Native AOT version (10000000 iterations)
  Time (mean ± σ):      3.956 s ±  0.041 s    [User: 2.848 s, System: 0.004 s]
  Range (min … max):    3.888 s …  4.016 s    10 runs

Summary
  Native AOT version ran 1.01 ± 0.04 times faster than .NET 8 version

PDF a PNG

En una única iteración de conversión de Banner Edulink One.pdf a PNG, la versión AOT se ejecuta aproximadamente 1,88 veces más rápido que la versión .NET 8:

Benchmark 1: .NET 8 version (1 iteration)
  Time (mean ± σ):      2.417 s ±  0.104 s    [User: 1.334 s, System: 0.116 s]
  Range (min … max):    2.295 s …  2.629 s    10 runs

Benchmark 2: Native AOT version (1 iteration)
  Time (mean ± σ):      1.288 s ±  0.011 s    [User: 0.573 s, System: 0.123 s]
  Range (min … max):    1.274 s …  1.310 s    10 runs

Para 20 iteraciones, la diferencia de velocidad es insignificante:

Benchmark 1: .NET 8 version (20 iterations)
  Time (mean ± σ):     25.048 s ±  0.223 s    [User: 13.278 s, System: 2.312 s]
  Range (min … max):   24.751 s … 25.423 s    10 runs

Benchmark 2: Native AOT version (20 iterations)
  Time (mean ± σ):     25.213 s ±  0.114 s    [User: 12.661 s, System: 2.275 s]
  Range (min … max):   25.042 s … 25.350 s    10 runs

Summary
  .NET 8 version ran 1.01 ± 0.01 times faster than Native AOT version

Para 3BigPreview.pdf, la versión Native AOT es más rápida incluso con 100 iteraciones:

Benchmark 1: .NET 8 version (100 iterations)
  Time (mean ± σ):     10.009 s ±  0.152 s    [User: 5.298 s, System: 0.567 s]
  Range (min … max):    9.677 s … 10.189 s    10 runs

Benchmark 2: Native AOT version (100 iterations)
  Time (mean ± σ):      8.336 s ±  0.070 s    [User: 3.405 s, System: 0.505 s]
  Range (min … max):    8.247 s …  8.459 s    10 runs

Summary
  Native AOT version ran 1.20 ± 0.02 times faster than .NET 8 version

Conclusión

Las aplicaciones Native AOT se inician más rápido que las .NET normales. Los puntos de referencia oficiales también muestran que las aplicaciones AOT ocupan menos memoria.

Sin embargo, después del inicio, las aplicaciones administradas suelen mostrar una mejor velocidad. Esto sucede porque JIT tiene acceso a la información de tiempo de ejecución. En aplicaciones de larga ejecución, puede regenerar un código más eficaz en función de la optimización dinámica guiada por perfiles y otras técnicas.

Los benchmarks de ASP.NET le permiten comparar distintas configuraciones desde la perspectiva del rendimiento. Sin embargo, los resultados dependen del sistema operativo y de la arquitectura del procesador. Debe ejecutar sus propios benchmarks en su entorno de destino para encontrar la configuración de implementación óptima.