Cargando...

Optimización del rendimiento con Span<T> y Memory<T> en C#

Aprende optimización de rendimiento en C# con Span<T> y Memory<T> para gestionar memoria de forma eficiente.

Introducidos en C# 7.2 y versiones posteriores, las estructuras Span<T> y Memory<T> optimizan el acceso a la memoria en escenarios de procesamiento de datos de alto rendimiento, evitando copias innecesarias. Gracias a estas estructuras, puedes trabajar con grandes arreglos, cadenas o búferes de bytes sin realizar asignaciones de memoria adicionales.


¿Qué es Span<T>?

Span<T> es una estructura basada en la pila que opera sobre la memoria utilizando el concepto de segmento (slice). Mantiene una referencia a una parte de un arreglo, cadena o memoria no administrada, sin copiar los datos. Sin embargo, al residir en la pila, no puede usarse en métodos async ni moverse al montón (heap).


using System;

class Program
{
    static void Main()
    {
        int[] numeros = { 10, 20, 30, 40, 50 };

        Span<int> segmento = numeros.AsSpan(1, 3); // 20,30,40
        segmento[0] = 99;

        Console.WriteLine(string.Join(", ", numeros)); 
        // Salida: 10, 99, 30, 40, 50 — apunta al mismo espacio de memoria
    }
}

Acceso sin copia con Slice

La principal ventaja de Span<T> es el acceso a subrangos de datos sin necesidad de copiar. Por ejemplo, si quieres trabajar con una parte específica de un gran arreglo de bytes, no es necesario crear un nuevo arreglo.


byte[] buffer = new byte[1000];
Span<byte> cabecera = buffer.AsSpan(0, 128); // sección de encabezado
Span<byte> datos = buffer.AsSpan(128);        // resto de los datos

// Diferentes partes del mismo arreglo sin copiar memoria
cabecera.Clear(); // limpia solo los primeros 128 bytes

stackalloc: Memoria temporal sin asignación en el heap

Con la palabra clave stackalloc puedes asignar memoria temporal en la pila en lugar del heap. Esta memoria se limpia automáticamente y no es gestionada por el recolector de basura (GC). Es muy eficiente en escenarios de procesamiento de datos pequeños y de alta frecuencia.


Span<int> numeros = stackalloc int[5];
for (int i = 0; i < numeros.Length; i++)
    numeros[i] = i * 10;

Console.WriteLine(string.Join(", ", numeros.ToArray()));

Nota: stackalloc solo funciona con Span<T> y debe usarse con tamaños pequeños. Existe riesgo de desbordamiento de pila (stack overflow) con datos grandes.


¿Qué es Memory<T>?

Memory<T> es la versión basada en el heap y segura para asincronía de Span<T>. Mientras que Span<T> solo vive en la pila, Memory<T> puede almacenarse en el heap y utilizarse de forma segura en escenarios asíncronos como async/await.


using System;

class Program
{
    static async Task Main()
    {
        byte[] datos = new byte[1024];
        Memory<byte> memoria = datos.AsMemory();

        await ProcesarAsync(memoria.Slice(100, 200));
    }

    static async Task ProcesarAsync(Memory<byte> data)
    {
        await Task.Delay(100); // simulación de operación asíncrona
        var span = data.Span;
        span.Fill(255); // acceso directo a la memoria
        Console.WriteLine("Segmento llenado.");
    }
}

Puedes acceder directamente a Span<T> mediante la propiedad Span de Memory<T>.


ReadOnlySpan<T> y ReadOnlyMemory<T>

Si los datos solo deben ser de lectura, puedes usar ReadOnlySpan<T> o ReadOnlyMemory<T>. Esto permite un acceso sin copia, pero impide la modificación de los datos.


ReadOnlySpan<char> span = "Hola Mundo".AsSpan();
Console.WriteLine(span.Slice(5)); // Mundo

Procesamiento de cadenas con Span

Usando Span<char>, puedes realizar divisiones de cadenas mucho más rápido que con Substring(). No se crea un nuevo objeto de cadena; simplemente se hace referencia a una parte de la cadena original.


ReadOnlySpan<char> texto = "1234-5678-9012-3456";
ReadOnlySpan<char> ultimos4 = texto.Slice(texto.Length - 4);

Console.WriteLine($"Últimos 4 dígitos: {ultimos4.ToString()}");

Comparación: Span vs ArraySegment

ArraySegment<T> representa una porción de un arreglo, pero solo funciona con arreglos. En cambio, Span<T> puede trabajar con diversas fuentes de datos como memoria, cadenas o punteros. Además, al ser una ref struct, Span<T> reduce significativamente la presión del GC.


int[] numeros = { 1, 2, 3, 4, 5 };
var segmento = new ArraySegment<int>(numeros, 1, 3);
Span<int> slice = numeros.AsSpan(1, 3);

Comparación de rendimiento

El siguiente ejemplo muestra la diferencia entre el corte clásico de cadenas y el uso de Span<T>. Substring() crea un nuevo objeto de cadena, mientras que AsSpan() solo hace referencia a la memoria existente.


string texto = "Prueba de rendimiento CSharp";

var parte1 = texto.Substring(7, 10);           // crea una nueva cadena
ReadOnlySpan<char> parte2 = texto.AsSpan(7, 10); // sin copia

Console.WriteLine(parte1);
Console.WriteLine(parte2.ToString());

Especialmente en operaciones con cadenas o bytes de gran tamaño, Span reduce significativamente la presión del GC.


Puntos importantes al usar Span y Memory


Ejemplo: División de un arreglo de bytes

Por ejemplo, al analizar un paquete de red o leer encabezados de archivo, procesar grandes arreglos de bytes sin copiar mejora significativamente el rendimiento. El siguiente ejemplo divide un paquete de datos en encabezado y cuerpo para procesarlos.


using System;

class Program
{
    static void Main()
    {
        byte[] paquete = new byte[1024];
        new Random().NextBytes(paquete);

        Span<byte> span = paquete.AsSpan();

        Span<byte> cabecera = span.Slice(0, 128);
        Span<byte> cuerpo = span.Slice(128);

        Console.WriteLine($"Longitud del encabezado: {cabecera.Length}");
        Console.WriteLine($"Longitud del cuerpo: {cuerpo.Length}");
    }
}

TL;DR

  • Span<T>: segmento de memoria rápido, sin copia, basado en la pila. (De corta duración, no compatible con async.)
  • Memory<T>: basado en el heap, compatible con async, referencia de memoria segura.
  • ReadOnlySpan / ReadOnlyMemory: acceso de solo lectura, preserva la integridad de los datos.
  • stackalloc: elimina la presión del GC para datos temporales pequeños.
  • Procesa grandes volúmenes de datos sin copiar para reducir significativamente la carga de CPU y GC.

Artículos relacionados