0

Manipulación de Memoria en C# 2: «Punteros vs. Span» El Duelo por el Low-Level en .NET

En la esquina roja, los Punteros: herencia directa de C, manipulación de direcciones de memoria y riesgo constante de Buffer Overflow. En la esquina azul, Span<T>: una estructura de tipo ref struct que ofrece seguridad de tipos y límites, pero con un rendimiento que pisa los talones al código no seguro.

El Factor Seguridad: El «Abismo» de los Punteros

La mayor diferencia no es la velocidad, sino el Safety.

  • Punteros: No tienen Bounds Checking. Si tienes un array de 10 elementos y pides el elemento 11 mediante aritmética de punteros *(p + 11), el CLR no te detendrá. Leerás basura o provocarás un cierre inesperado del proceso.
  • Span<T>: Implementa un acceso de alto rendimiento que sí verifica los límites, pero lo hace de forma tan optimizada que el JIT (Just-In-Time compiler) a menudo elimina el costo de esa verificación en bucles cerrados.

Escenario de Error: Desbordamiento de Buffer

// CON PUNTEROS (Peligro de muerte)
unsafe void Peligro(int* p) {
    p[1000] = 99; // Si el array mide 10, acabas de corromper la memoria del proceso.
}

// CON SPAN (Protección total)
void Seguro(Span<int> s) {
    s[1000] = 99; // Lanza IndexOutOfRangeException. El proceso sobrevive.
}

Flexibilidad: ¿Dónde vive la memoria?

Aquí es donde Span<T> brilla. Mientras que los punteros requieren bloques fixed para memoria administrada (el Heap), Span<T> es agnóstico.

MemoriaPunteros (unsafe)Span<T> / ReadOnlySpan<T>
Stack (stackalloc)SoportadoSoportado (Muy eficiente)
Heap (new byte[])Requiere fixed (Anclaje)Nativo (Sin anclaje manual)
Memoria Nativa (Interop)NativoVía Span<T> constructor
StringsSolo mediante fixed char*ReadOnlySpan<char> (Sin copias)

El problema del Puntero: Al usar fixed para usar un puntero sobre un objeto en el Heap, estás impidiendo que el Garbage Collector mueva ese objeto. Si abusas de esto, generas fragmentación de memoria, degradando el rendimiento general de tu aplicación de forma silenciosa. Span<T> no ancla la memoria, se mueve con ella.

El Rendimiento: ¿Realmente son más rápidos los punteros?

En 2025, la respuesta es: Casi nunca.

Gracias a las optimizaciones del compilador JIT modernas, un bucle que recorre un Span<T> es prácticamente idéntico en lenguaje ensamblador a uno que usa punteros. El JIT aplica una técnica llamada Bounds Check Elimination, donde si detecta que el índice nunca superará el tamaño del Span, elimina la validación en tiempo de ejecución.

// ENFOQUE TRADICIONAL CON PUNTEROS
public unsafe void UpdatePointers(int* data, int length) {
    for (int i = 0; i < length; i++) {
        data[i] += 1;
    }
}

// ENFOQUE MODERNO CON SPAN
public void UpdateSpan(Span<int> data) {
    for (int i = 0; i < data.Length; i++) {
        data[i] += 1; // El JIT elimina el check de límites aquí
    }
}

¿Cuándo usar cada uno?

Usa Punteros (unsafe) SI:

  1. Estás llamando a una API de C++ o Win32 que requiere explícitamente un puntero (void*, int*).
  2. Estás construyendo una estructura de datos personalizada extremadamente compleja (como un árbol B+ persistente en memoria) donde la aritmética de punteros es el lenguaje natural.
  3. Cada nanosegundo es crítico y has medido (con BenchmarkDotNet) que el JIT no está optimizando el Span.

Usa Span<T> SI:

  1. Estás haciendo Parsing de texto o JSON (usa ReadOnlySpan<char>).
  2. Trabajas con Buffers de red o archivos.
  3. Quieres alto rendimiento sin arriesgarte a vulnerabilidades de seguridad (Buffer Overflows).
  4. En el 95% de los casos de desarrollo moderno en .NET.

Notas

  • Refactoriza tu código unsafe: Si tienes métodos antiguos que usan punteros solo para saltarse los límites de un array, cámbialos a Span<T>. Obtendrás seguridad gratuita con una pérdida de rendimiento nula.
  • Domina stackalloc con Span: Puedes asignar memoria ultra rápida en la pila sin usar unsafe:
Span<int> buffer = stackalloc int[128];

Span<int> buffer = stackalloc int[128];

  • Cuidado con el Scope: Recuerda que Span<T> es un ref struct. No puede ser un campo de una clase ni usarse en métodos async. Si necesitas persistencia entre llamadas asíncronas, usa Memory<T>.

Conclusiones

En el ecosistema moderno de .NET, los punteros son una herramienta quirúrgica, no el martillo universal. Úsalos solo cuando realmente lo necesites: interoperabilidad con C++ o Win32, estructuras de datos de muy bajo nivel donde la aritmética de memoria es esencial, o escenarios ultra críticos donde mediste con BenchmarkDotNet y comprobaste que no hay alternativa más rápida. Fuera de eso, Span<T> gana por diseño.

Fernando Sonego

Deja una respuesta

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