← maurobernal.com.ar

Rendimiento extremo en .NET 10: Stack allocation, Native AOT y el GC que trabaja menos

El rendimiento de .NET siempre fue bueno. Pero en los últimos dos años, con .NET 9 y .NET 10, el equipo fue más allá: Stack allocation inteligente para objetos pequeños, Native AOT que genera binarios sin runtime, y un GC que hace menos trabajo porque el runtime hace más con menos memoria. En APIs que proceso millones de requests por día, estos cambios se notan.

Stack vs Heap: la distinción que importa para el rendimiento

El Garbage Collector de .NET es excelente, pero tiene un costo: cada objeto en el heap eventual­mente debe ser recolectado. La forma más eficiente de evitar ese costo es no poner cosas en el heap en primer lugar. .NET 10 hace esto automáticamente en más situaciones.

// .NET 10: el runtime puede asignar arrays pequeños en el stack automáticamente
// cuando el compilador puede probar que no "escapan" del scope

// Caso 1: array de tamaño conocido en compilación que no escapa
void ProcesarLote()
{
    // .NET 10 puede convertir esto en stack allocation transparentemente
    int[] buffer = new int[8];  // pequeño, scope limitado
    for (int i = 0; i < buffer.Length; i++)
        buffer[i] = i * 2;
    // buffer no escapa a este método → candidato a stack allocation
}

// Caso 2: stackalloc explícito para control total
void ProcesarDatos(ReadOnlySpan<byte> datos)
{
    // Siempre en el stack, sin GC pressure
    Span<byte> buffer = stackalloc byte[256];
    datos.CopyTo(buffer);
    // Procesamiento...
}

// Caso 3: ArrayPool para buffers grandes que SÍ van al heap
// (cuando stackalloc no alcanza)
var pool = ArrayPool<byte>.Shared;
byte[] rentado = pool.Rent(4096);
try
{
    // usar rentado...
}
finally
{
    pool.Return(rentado);  // devolver al pool en lugar de GC
}

Native AOT: binarios sin runtime instalado

Native AOT (Ahead-Of-Time compilation) compila tu aplicación .NET a código nativo — un ejecutable que no requiere el runtime de .NET instalado. En .NET 10, el soporte mejoró significativamente: más APIs compatibles, binarios más pequeños y arranque más rápido.

# Publicar con Native AOT
# En el .csproj:
<PropertyGroup>
  <PublishAot>true</PublishAot>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

# Publicar para Linux x64
dotnet publish -r linux-x64 -c Release

# Resultado típico en .NET 10:
# Tamaño del binario: ~8-12 MB (vs ~200 MB con runtime incluido)
# Tiempo de arranque: ~5ms (vs ~100-300ms con JIT)
# Memoria RSS inicial: ~15 MB (vs ~50 MB con JIT)

Las limitaciones de Native AOT siguen existiendo — no soporta reflection dinámica, ciertas operaciones de serialización requieren source generators — pero en .NET 10 el porcentaje de código compatible aumentó considerablemente. Mis Minimal APIs y Workers services migran sin problemas.

Mejoras del GC en .NET 10

// Configurar el GC para workloads específicos
// En appsettings.json o como variables de entorno:

// Para APIs de alta frecuencia (latencia baja):
// DOTNET_GCConserveMemory=0 (default — más GC, menos memoria)

// Para servicios de larga duración (memoria controlada):
// DOTNET_GCConserveMemory=9 (agresivo en liberar memoria)

// Programmaticamente:
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency; // Para APIs

// .NET 10: Dynamic Heap Regions
// El GC puede ajustar el tamaño de las regiones del heap en tiempo de ejecución
// según los patrones de uso observados — sin configuración manual

Tensor Primitives: operaciones SIMD de alto nivel

Introducido en .NET 9 y expandido en .NET 10, System.Numerics.TensorPrimitives expone operaciones vectorizadas (SIMD) con una API de alto nivel. Útil para procesamiento de señales, ML, operaciones financieras en bulk.

using System.Numerics.Tensors;

float[] precios = [100f, 200f, 150f, 300f, 250f];
float[] descuentos = [0.1f, 0.2f, 0.15f, 0.05f, 0.25f];
float[] preciosFinales = new float[precios.Length];

// Multiplicación vectorizada — usa SIMD automáticamente
TensorPrimitives.Multiply(precios, descuentos, preciosFinales);

// Suma de todos los elementos — también vectorizada
float sumaTotal = TensorPrimitives.Sum(precios);

// Operaciones estadísticas
float maximo = TensorPrimitives.Max(precios);
float minimo = TensorPrimitives.Min(precios);

Números reales de mis APIs

Métrica.NET 8.NET 10Mejora
Memoria RSS (pod idle)~85 MB~65 MB~24% menos
P99 latencia (API simple)~12 ms~9 ms~25% menos
Throughput (req/s)~45.000~58.000~29% más
Tiempo arranque cold~280 ms~200 ms~28% menos

Estos números son de una API de gestión interna con carga moderada. En workloads más intensivos los delta son mayores. En workloads livianos, menores. Pero la dirección es siempre la misma: .NET 10 hace más con menos.


← LINQ en .NET 9 y .NET 10: CountBy, AggregateBy, Index() y las mejoras que cambian cómo consultás datos | Serie .NET 8 → .NET 10 | Próximo: dotnet run script.cs y las nuevas herramientas de .NET 10: OpenAPI, HttpClient y más →

Comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.