Alla scoperta di Span<T>

Con il net core 2.1 e C# 7.2 è stato introdotta una nuova tipologia di struct: lo Span<T> ed il ReadOnlySpan<T>, dove T rappresenta un tipo (es. array, stringhe, char, interi,..). Possiamo immaginarlo come una “finestra” su una lista di dati già allocati in memoria ed è particolarmente efficiente poichè è una struttura stack-only.

Cosa significa che è una struttura stack-only?

Iniziamo con il capire cosa è lo stack e cosa è l’heap.

Lo stack è usato per memorizzare:

  • Variabili locali
  • Dati temporanei come parametri dei metodi.
  • Valori immediati come tipi primitivi (int, float, Span<T>, etc.).
  • E’ possibile allocare oggetti piccoli

Quando un metodo termina, tutti i dati allocati nello stack vengono automaticamente deallocati.

L’heap è usato per memorizzare:

  • Oggetti complessi (come le classi), array, List<T>.
  • Dati che devono sopravvivere oltre il ciclo di vita di un metodo (es. oggetti condivisi).
  • Può memorizzare oggetti di grandi dimensioni

L’heap richiede il garbage collector per deallocare la memoria inutilizzata.

Limiti e caratteristiche di uno Span<T>

Ora che abbiamo visto la differenza tra i due tipi di memoria e dove viene memorizzato il nostro Span<T> iniziamo a capire il perimetro entro il quale utilizzarlo poichè non sempre è la scelta corretta. Iniziamo a definire i suoi vantaggi:

Performance: essendo allocato solamente sullo stack è più veloce

Slicing e memoria : è possibile andare ad estrarre una porzione (slice) di dati senza che questi vengano copiati in una seconda parte di memoria. E’ come se venisse creata una finestra su quella porzione di dati. Quando viene effettuato uno slicing su un oggetto di tipo Array<T> invece viene generata una nuova istanza dell’array contenente solamente la porzione dei dati (e ci ritroveremmo in memoria due array: quello originale e quello porzionato)

string[] array = { "uno", "due", "tre", "quattro", "cinque" };
Span<string> span = array.AsSpan();

//Slicing
Span<string> slicedSpanArray = span[1..4]; // { "due", "tre", "quattro" }

slicedSpanArray[0] = "DUE"; //Modifica anche l'array originale
Console.WriteLine(array[1]); // "DUE"

Quali sono i suoi limiti?

Contesto: lo Span<T> può essere utilizzato solamente nel contesto corrente (es. una funzione) e non può uscire da esso. Non puoi neanche usare Span<T> come property di una classe.

Dimensione limitata: Lo stack è piccolo (generalmente 1MB per thread), non puoi usarlo per dati molto grandi.

Vediamo ora di confrontare Span<T> con un array o un List<T>

CaratteristicaArrayList<T>Span<T>
Contenitore di datiSiSiNo (punta a dei dati in memoria)
AllocazioneHeapHeapStack
Modifica dei datiSiSiSì (se consentito)
SliceVerrà creato un nuovo oggetto con la porzione di datiVerrà creato un nuovo oggetto con la porzione di datiNon verrà creato nessun oggetto ma la “finestra” sarà limitata alla porzione desiderata
Thread-safeNoNoNo

Prima di trarre le conclusioni vediamo un esempio di codice.

In questo esempio abbiamo inizialmente un array di interi, composto da 10 elementi, successivamente creo un elemento Span di 5 elementi partendo dall’indice 2 dell’array originale: come abbiamo visto prima non verrà creata una copia dell’array ma andremo a lavorare su una “vista” dei dati. Successivamente andiamo a ciclare gli elementi dello span, andando a raddoppiare i suoi valori. Per concludere andiamo a ciclare tutti i valori del nostro array di partenza, scoprendo che le trasformazioni effettuate tramite lo span si sono riflesse anche sul nostro array originale (o per meglio dire hanno lavorato direttamente sui dati in memoria dell’array originale)

var originalArray = new int[] { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };
//Creo uno span a partire da originalArray, ma solo sugli indici 2,3,4,5,6
var sliceSpan = new Span<int>(array, 2, 5); 

//Ciclo l'ggetto sliceSpan (che è una porzione span dell'array originalArray)
//e applico una trasformazione agli elementi
for (int ctr = 0; ctr < sliceSpan.Length; ctr++)
    sliceSpan[ctr] *= 2;

//Ciclo l'array originale
foreach (var value in originalArray)
    Console.Write($"{value},  ");
Console.WriteLine();

//Indice:         0  1  2   3   4   5   6   7   8   9
//originalArray:  2, 4, 6,  8,  10, 12, 14, 16, 18, 20
//Output:         2, 4, 12, 16, 20, 24, 28, 16, 18, 20

Conclusioni

Span<T> è uno strumento potente per ottimizzare operazioni che richiedono performance elevate e bassa allocazione in memoria. Tuttavia, non è una soluzione universale: bisogna usarlo nei contesti giusti, dove i vincoli dello stack non rappresentano un problema. Per tutti gli altri casi, array e liste rimangono la scelta più sicura e flessibile.

Condividi questo articolo
Shareable URL
Post precedente

Ritentare query fallite con Microsoft.Data.SqlClient

Prosimo post

Come creare un extension method per LINQ

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Leggi il prossimo articolo