← maurobernal.com.ar

Etiqueta: memory

  • 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 →

Tags

tsql (27)mssql (26)sql (20)devops (20)dotnet (18)docker (15)performance (14)contenedores (11)dotnet10 (10)linux (9)csharp (8)microservicios (7)angular (7)angular21 (7)sql server (6)issabel (6)docker-compose (6)typescript (6)mysql (5).NET (5)