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
Span<T>vive en la pila; no puede usarse en métodos asíncronos ni escapar de lambdas.Memory<T>vive en el heap; debe preferirse en escenarios asíncronos.- La vida útil de los datos subyacentes debe ser mayor que la del
Span(de lo contrario, la memoria puede corromperse). stackallocdebe usarse solo para datos pequeños (por ejemplo, < 1 KB).- Es altamente eficaz en procesamiento de texto, red, JSON o datos binarios de alto rendimiento.
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
Arreglos (Arrays) en C#
Aprende arreglos en C#: declaración, índices, recorridos con bucles y operaciones básicas con ejemplos prácticos.
Código no seguro y punteros en C#
Aprende código no seguro y punteros en C# para trabajar con direcciones de memoria y operaciones de bajo nivel.
El concepto de los generadores de código en C# (C# 9+)
Aprende generadores de código en C# para generar código en tiempo de compilación y mejorar el rendimiento.
Genéricos en C# (List<T>, Dictionary<TKey,TValue>)
Aprende genéricos en C# (List<T>, Dictionary<TKey,TValue>) para escribir código reutilizable y seguro de tipos con ejemplos.
Gestión de memoria y Garbage Collector en C#
Aprende gestión de memoria y garbage collector en C# para comprender el ciclo de vida de objetos y la limpieza de memoria.
Structs en C# – Diferencias con las clases
Aprende las diferencias entre structs y clases en C#, incluyendo modelo de memoria, herencia y rendimiento.
Tipos de datos básicos en C#
Tipos de datos básicos en C#: numéricos, de texto, lógicos, orientados a objetos y anulables.