0

Span la estructura de C# olvidada

Span es un de esas funcionalidades o características que pasaron desapercibida en C#. Span es una estructura introducida en la versión 7 de C# que nos permite representar una región contigua de memoria arbitraria como un objeto de clase primaria.

Span es especialmente útil cuando nos encontramos trabajando con un gran volumen de datos permitiéndonos hacer un uso más eficiente de la memoria logrando un excelente rendimiento.

Utilizaremos Span pasta para declarar un variable del tipo Span indicando a T como el tipo de elemento en el Span. Digamos que queremos declarar y array de números, pero en su lugar utilizaremos Span.

Span<int> numbers = new int[] { 3, 4, 9, 11, 15 };

Una gran característica es que permite usar la memoria más eficientemente evitando copiar datos innecesarios. En algunos casos, nos permite acceder directamente a los datos en memoria, sin tener que extraerlos o copiar a una matriz o array temporalmente administrada e independiente. Esto es fundamental cuando tenemos mucha cantidad de datos ya que dependiendo de lo que necesitemos tomar, ocuparemos más memoria.

Por otro lado, hacemos mejor uso en la manipulación de los datos. Por ejemplo, cuando usamos el método Slice(), que nos permite tomar un segmento a partir de un índice, creará un subconjunto del conjunto de datos original. Es útil cuando debemos acceder a partes específicas de un conjunto de datos sin tener que copiar nuevamente los datos a otro lugar.

Span<int> lastThreeNumbers = numbers.Slice(1, 3);

Además de slice tenemos varias propiedades, métodos y funciones de comparación disponibles.

Propiedades
EmptyDevuelve un objeto Span<T> vacío.
IsEmptyDevuelve un valor que indica si el elemento Span<T> actual está vacío.
Item[Int32]Obtiene el elemento en el índice basado en cero especificado.
LengthDevuelve la longitud del intervalo actual.
Métodos
Clear()Borra el contenido de este objeto Span<T>.
CopyTo(Span<T>)Copia el contenido de este elemento Span<T> en un elemento Span<T> de destino.
Equals(Object)Obsoleto.No se admiten llamadas a este método.
Fill(T)Rellena los elementos de este intervalo con un valor especificado.
GetEnumerator()Devuelve un enumerador para este elemento Span<T>.
GetHashCode()Obsoleto.Produce una excepción NotSupportedException.
GetPinnableReference()Devuelve una referencia a un objeto de tipo T que se puede usar para anclar.Este método está diseñado para admitir compiladores de .NET y no está diseñado para que el código de usuario lo llame.
Slice(Int32)Forma un segmento fuera del intervalo actual que comienza en un índice especificado.
Slice(Int32, Int32)Forma un segmento fuera del intervalo actual a partir de un índice especificado durante una longitud determinada.
ToArray()Copia el contenido de este intervalo en una nueva matriz.
ToString()Devuelve la representación en forma de cadena de este objeto Span<T>.
TryCopyTo(Span<T>)Intenta copiar el elemento Span<T> actual en un destino Span<T> y devuelve un valor que indica si la operación de copia se ha realizado correctamente.
Operadores
Equality(Span<T>, Span<T>)Devuelve un valor que indica si dos objetos Span<T> son iguales.
Implicit(ArraySegment<T> to Span<T>)Define una conversión implícita de un elemento ArraySegment<T> en Span<T>.
Implicit(Span<T> to ReadOnlySpan<T>)Define una conversión implícita de un elemento Span<T> en ReadOnlySpan<T>.
Implicit(T[] to Span<T>)Define una conversión implícita de una matriz en Span<T>.
Inequality(Span<T>, Span<T>)Devuelve un valor que indica si dos objetos Span<T> no son iguales.

Puedes consultar las extensión de métodos desde este link

Ejemplos

Veamos un ejemplo de uso. Como comentamos, cuando trabajamos con grandes volúmenes de datos Span nos permitirá acceder y manipular los datos de manera eficiente. Si tenemos una matriz de números enteros y deseamos realizar una operación cada 5 elementos podemos utilizar Slice(). Este nos ayudará a crear un nuevo Span que contenga solo los elementos que deseamos y luego realizar la operación requerida.

// Crear el array
int[] bigArray = new int[500000];

// Llenamos nuestro array con los numeros
for (int i = 0; i < bigArray.Length; i++)
{
    bigArray[i] = i;
}

// Creamos el Span
Span<int> lbigArraySpan = largeArray;

// Creamos el subconjunto de datos
Span<int> _elements = bigArraySpan.Slice(9, bigeArraySpan.Length / 5);

// Perform some operation on every tenth element
for (int i = 0; i < _elements .Length; i++)
{
    _elements [i] *= 2;
}

Cuando trabajamos con datos almacenados en memoria de forma administrada Span nos será útil. Si estamos trabajando con una matriz de bytes que está relacionada con un archivo de disco podemos acceder los datos y manipularlos directamente sin intermediarios y sin la necesidad de volverlos a copiar.

byte[] _file = File.ReadAllBytes("C:\\bigFile.dat");

Span<byte> _fileSpan = _file;

for (int i = 0; i < _fileSpan.Length; i++)
{
    _fileSpan[i] ^= 0xFF;
}

Si tenemos algoritmos críticos que necesitan un excelente rendimiento accediendo directamente a regiones contiguas de memoria Span es una excelente opción. Si implementamos un algoritmo de ordenación, podemos representar los datos que se ordenarán y luego, manipular los datos directamente.

int[] _data = { 3, 4, 2, 7, 3, 2, 6, 9, 0 };

Span<int> _dataSpan = _data;

for (int i = 0; i < _dataSpan.Length - 1; i++)
{
    for (int j = i + 1; j < _dataSpan.Length; j++)
    {
        if (_dataSpan[i] > _dataSpan[j])
        {
            int temp = _dataSpan[i];
            _dataSpan[i] = _dataSpan[j];
            _dataSpan[j] = temp;
        }
    }
}

Limitaciones

No todo es color de rosa. Ten presente que es una estructura de tipo ref que se asigna en la pila en lugar de en managed heap. Al ser ref, posee una serie de restricciones que garantizan que no se puedan pasar al managed heap. Tampoco es posible convertir el tipo de valor a cualquier otro tipo de la interfaz que implementa el tipo, no se pueden asignar objetos y no es posible usar tipos dynamic.

Como su base es de tipo pila, es la mejor opción para escenarios donde requerimos referencias a un bugger. Por ejemplo, podemos complementar su uso con los tipos System.Mermory<T> y System.ReadOnlyMemory<T>.

Conclusiones

En la empresa, para la cual trabajo, utilizamos esta estructura para manejar las geoposiciones de los vehículos de la compañía. Cada vehículo reporta su posición cada 1 minuto creando un gran volumen de datos registrando sus rutas a diario. Reducimos un 25% promedio el tiempo de resolución de las rutas y hasta en algunos casos un 80%.

Espero que te haya sido útil. Ten invito a probar esta estructura de C#. Cuéntame en los comentarios como te fue.

Fernando Sonego

Deja una respuesta

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