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 regular? ¿Puede AOT superar a JIT? ¿Cómo se comparan aplicaciones Native AOT?

Este artículo forma parte de una serie sobre Native AOT en .NET. Si no estás familiarizado con Native AOT, lee 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 distintas opciones de implementación de .NET para aplicaciones ASP.NET sencillas.
Después, aprenderás a ejecutar tus propios benchmarks usando BenchmarkDotNet y las herramientas hyperfine. Estos benchmarks te permiten medir la velocidad del código en tu entorno.
Benchmarks de ASP.NET
El equipo de ASP.NET mantiene una infraestructura sólida para pruebas de rendimiento. Prueban varios escenarios en distintos entornos.
Nos interesan especialmente los benchmarks de Native AOT. La fuente principal de información es el siguiente panel de PowerBI. Los datos allí se basan en 3 "ballenas": aplicaciones de prueba, escenarios de implementación y métricas.
Aplicaciones de prueba
Puedes 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 aplicación:
- Stage1 - una API mínima basada en HTTP y JSON. El código fuente de la aplicación está 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 distintos entornos. Por el momento, los benchmarks usan máquinas virtuales de 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 distintas configuraciones. Una aplicación en alguna configuración define un "escenario".
Puedes mantener presionada la tecla Ctrl (o ⌘) para seleccionar varios escenarios o entornos en el panel de PowerBI.
Métricas
Los benchmarks recopilan métricas fundamentales para cada aplicación implementada. Por ejemplo, las pruebas miden el número de solicitudes por segundo (RPS), el tiempo de inicio y el conjunto de trabajo máximo de memoria.
Eso nos permite comparar valores de métricas para distintas configuraciones de la misma aplicación.
Comparación de rendimiento
Compararemos los escenarios StageX con StageXAot y StageXAotSpeedOpt. Usan 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 usan la variable de entorno DOTNET_GCDynamicAdaptationMode=1.
Los escenarios StageXAotSpeedOpt permiten estimar el impacto de la configuración OptimizationPreference = Speed.
También puedes revisar los escenarios StageXTrimR2RSingleFile. Dichos escenarios corresponden a una implementación de ReadyToRun con recorte, que es otra forma de compilación ahead-of-time en .NET. A veces, es una buena alternativa a Native AOT.
Estos son los resultados actuales de comparación de rendimiento 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. Eso es cierto tanto para las aplicaciones Stage1 como para las Stage2 y en 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 1.5 - 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
Valores de RPS más altos significan una aplicación más rápida. La aplicación ligera Stage1 normalmente maneja alrededor de 800-900K solicitudes por segundo. La aplicación más grande Stage2 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 cercana. Pero, por lo general, se sitúa entre Stage2 y Stage2Aot. Estos son los resultados típicos:
| Escenario | RPS |
|---|---|
| Stage2 | 235,008 |
| Stage2AotSpeedOpt | 215,637 |
| Stage2Aot | 194,264 |
Los resultados para 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 tanto, el entorno y el código de la aplicación pueden afectar significativamente la velocidad. Tiene sentido ejecutar tus propios benchmarks para estimar los beneficios de Native AOT para tu proyecto. Escribamos benchmarks personalizados sin la infraestructura de pruebas de Microsoft.
Pruebas de rendimiento de aplicaciones Native AOT
Usaremos 2 tipos de benchmarks. El primero se basa en BenchmarkDotNet, la biblioteca popular para medir el rendimiento 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 benchmarks comparan la velocidad total, incluido el tiempo de inicio.
Aquí no compararemos el consumo de memoria. Por el momento, el diagnoser NativeMemoryProfiler de BenchmarkDotNet no admite el runtime Native AOT. hyperfine tampoco registra actualmente el uso de memoria.
Puedes descargar el código fuente del repositorio NativeAotBenchmarks en GitHub. Te animamos a probarlo en tu entorno. Este artículo describe resultados de una laptop con Windows 11, procesador Intel Core i9-13900H y 16 Gb de RAM.
Asegúrate de ejecutar los benchmarks correctamente. Estas son las recomendaciones habituales:
- Usa la compilación Release.
- Cierra todas las aplicaciones excepto el proceso del benchmark. Por ejemplo, desactiva el antivirus, cierra Visual Studio y un navegador web.
- Mantén la laptop conectada a la corriente y usa el modo de máximo rendimiento.
- Usa los mismos datos de entrada en los escenarios que se comparan.
Casos de prueba
Benchmarkearemos 2 escenarios en .NET 8:
1. Código C# sencillo para la compresión de una 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 de conversión de PDF a PNG que usa Docotic.Pdf.
Prerrequisitos
Instala los requisitos previos para la implementación de .NET Native AOT.
Instala hyperfine para ejecutar los benchmarks correspondientes.
Para los benchmarks de PDF a PNG, obtén una clave de licencia gratuita con tiempo limitado en la página Descargar biblioteca PDF para C# .NET. Debes aplicar la clave de licencia en Helper.cs.
BenchmarkDotNet
Estos benchmarks están ubicados en el proyecto NativeAotBenchmarks. Comparamos resultados para RuntimeMoniker.NativeAot80 y RuntimeMoniker.Net80. De forma predeterminada, BenchmarkDotNet compila código Native AOT con la configuración OptimizationPreference=Speed.
BenchmarkDotNet realiza 6 o más iteraciones de calentamiento. Eso ayuda a JIT a precompilar código y recopilar algunas estadísticas. Por tanto, estos benhmarks excluyen el tiempo de inicio de la comparación.
Compresión de cadenas
El benchmark CompressString para compresión de cadenas usa 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 usarían cadenas de entrada diferentes. Es posible usar cadenas aleatorias, pero debes inicializar un generador aleatorio con la misma semilla.
La versión Native AOT se ejecuta unas 1.08 veces más rápido que la versión .NET 8:
| Método | Runtime | Media | Error | Desv. estándar |
|---|---|---|---|---|
| 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 memoria. Eso permite excluir la interacción con el sistema de archivos. Las operaciones de E/S con un disco pueden sesgar 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 dpi y requiere más tiempo de procesamiento. La versión .NET 8 es ligeramente más rápida para este documento:
| Método | Runtime | Media | Error | Desv. estándar |
|---|---|---|---|---|
| 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 simple. Se convierte a un PNG de 300 dpi. Y la velocidad es casi igual:
| Método | Runtime | Media | Error | Desv. estándar |
|---|---|---|---|---|
| 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 benchmarks están ubicados en el proyecto NativeAotTestApp. El proyecto no usa la configuración OptimizationPreference=Speed. Puedes habilitarla en NativeAotTestApp.csproj: <OptimizationPreference>Speed</OptimizationPreference>
Usa el script benchmark.bat para ejecutar pruebas en Windows. Puedes convertirlo a Bash para sistemas operativos basados en Unix/Linux. El script compila las 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"
Las ejecuciones de calentamiento en hyperfine ayudan a iniciar las aplicaciones de prueba con cachés de disco "calientes". A diferencia de BenchmarkDotNet, el calentamiento de hyperfine no ayuda a JIT. Por tanto, los benchmarks de hyperfine 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 da a JIT oportunidades para recopilar más estadísticas en tiempo de ejecución y generar código más rápido.
Una situación habitual es la siguiente. La primera vez, ejecutas benchmarks con una sola iteración. Una versión Native AOT funciona mucho más rápido. Luego, ejecutas los mismos benchmarks con varias iteraciones y la velocidad total de ambas versiones se iguala. Eso significa que, después del inicio, una versión administrada en realidad es 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 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
Para una sola iteración de la conversión de Banner Edulink One.pdf a PNG, la versión AOT se ejecuta unas 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 en comparación con .NET regular. Los benchmarks oficiales también muestran que las aplicaciones AOT tienen huellas de memoria más pequeñas.
Pero después del inicio, las aplicaciones administradas suelen mostrar mejor velocidad. Eso ocurre porque JIT tiene acceso a información de tiempo de ejecución. En aplicaciones de larga duración, puede regenerar código más eficaz basado en optimización adaptativa guiada por perfiles en tiempo de ejecución y otras técnicas.
Los benchmarks de ASP.NET te permiten comparar distintas configuraciones desde la perspectiva del rendimiento. Sin embargo, los resultados dependen de un sistema operativo y de una arquitectura de procesador. Debes ejecutar tus propios benchmarks en tu entorno de destino para encontrar la configuración de implementación óptima.