0

Manipulación de Memoria en C# 3: Benchmark de Procesamiento Masivo

En este escenario, realizaremos una operación simple (sumar un valor a cada elemento) sobre un array de 1,000,000 de enteros. Compararemos tres enfoques:

  1. Indexación Estándar: El control (C# seguro).
  2. Punteros (unsafe): El método «al metal».
  3. Span<T>: La alternativa moderna.

El Código del Experimento

Para ejecutar esto, necesitarás el paquete NuGet BenchmarkDotNet. Aquí está la implementación técnica:

using BenchmarkDotNet.Attributes;
using System.Runtime.InteropServices;

[MemoryDiagnoser] // Monitorea si hay asignaciones en el Heap
public class ConcurrencyBenchmark
{
    private int[] _data;
    private const int Size = 1_000_000;

    [GlobalSetup]
    public void Setup()
    {
        _data = Enumerable.Range(0, Size).ToArray();
    }

    // 1. MÉTODO SEGURO (ARRAY)
    [Benchmark(Baseline = true)]
    public void StandardArray()
    {
        for (int i = 0; i < _data.Length; i++)
        {
            _data[i] += 1;
        }
    }

    // 2. MÉTODO UNSAFE (PUNTEROS)
    [Benchmark]
    public unsafe void PointerAccess()
    {
        fixed (int* p = _data)
        {
            int* ptr = p;
            for (int i = 0; i < Size; i++)
            {
                *ptr += 1;
                ptr++;
            }
        }
    }

    // 3. MÉTODO SPAN (EL PRETENDIENTE)
    [Benchmark]
    public void SpanAccess()
    {
        Span<int> span = _data;
        for (int i = 0; i < span.Length; i++)
        {
            span[i] += 1;
        }
    }
}

Análisis del Desempeño (Resultados Esperados en .NET 8/10)

Al ejecutar este benchmark en un entorno de producción (Release mode), los resultados suelen dejar a muchos programadores Senior con la boca abierta:

MétodoMedia (ms)RatioComentario
StandardArray0.45 ms1.00El JIT hace un buen trabajo, pero el Bounds Check suma.
PointerAccess0.38 ms0.84Más rápido, pero el costo de fixed penaliza si el array es corto.
SpanAccess0.39 ms0.86Empate técnico. La diferencia es despreciable vs la seguridad ganada.

¿Por qué el Puntero no «aplasta» al Span?

Aquí entra el concepto de SIMD (Single Instruction, Multiple Data) y Loop Unrolling.

En 2026, el compilador JIT es tan inteligente que cuando ve un Span<T> o un array estándar, intenta vectorizar el bucle. Esto significa que en lugar de sumar un número a la vez, el procesador suma 4 o 8 números en una sola instrucción de CPU.

  • Punteros: Al manipular la dirección manualmente (ptr++), a veces podemos romper accidentalmente la capacidad del compilador para optimizar mediante vectorización si la lógica es muy compleja.
  • Span: Proporciona una estructura clara que el compilador entiende perfectamente, facilitando la optimización automática.

Escenario de «Falla y Error»: El costo oculto del fixed

Si intentas usar punteros en un bucle que se ejecuta miles de veces sobre arrays pequeños, tu rendimiento será peor que el código normal.

¿Por qué? Porque la sentencia fixed le dice al Garbage Collector: «No muevas este objeto». Esto requiere una operación de registro en la tabla de handles del GC. Si haces esto constantemente en un entorno multi-hilo, generas una fricción interna que destruye cualquier ganancia de nanosegundos que hayas obtenido con el puntero.

Notas

  1. Mide antes de Optimizar: Nunca uses unsafe basándote en una corazonada. Como viste en el benchmark, la diferencia suele ser mínima (menos del 15% en muchos casos).
  2. Elige Seguridad por Defecto: Usa Span<T> como tu herramienta de alto rendimiento estándar. Es más fácil de leer, más difícil de romper y amigable con el GC.
  3. Micro-optimizaciones: Solo baja a punteros si estás implementando un algoritmo que requiere Aritmética de Punteros no lineal (por ejemplo, saltar por la memoria de forma impredecible) donde Span podría tener un overhead por el check de límites.

Conclusiones

En rendimiento, la intuición es un mal consejero y la evidencia es la única autoridad. Antes de recurrir a unsafe, mide. En la mayoría de los escenarios reales, la diferencia es marginal y no justifica el costo en complejidad y riesgo. Por defecto, elige Span<T>: ofrece alto rendimiento, seguridad de memoria, mejor legibilidad y una integración natural con el GC y el JIT moderno. Es la opción profesional para el 95% de los casos.

Reserva los punteros para situaciones verdaderamente excepcionales: algoritmos donde la aritmética de memoria no lineal es parte esencial del diseño y cada instrucción cuenta. Optimizar no es escribir código más peligroso; es tomar decisiones informadas. Primero claridad, luego medición, y recién al final micro-optimización. Esa es la mentalidad de ingeniería madura en .NET.

Fernando Sonego

Deja una respuesta

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