Memory e stackalloc:

Span, Memory e stackalloc: scrivere C# ad alte prestazioni senza GC overhead

In .NET il garbage collector (GC) gestisce la memoria in modo automatico, evitando memory leak e semplificando la vita agli sviluppatori. Tuttavia, in applicazioni ad alte prestazioni (real-time, networking, finanza, AI, elaborazione di dati massivi), il GC può diventare un collo di bottiglia.

Per rispondere a questa esigenza, Microsoft ha introdotto strumenti come Span<T>, Memory<T> e stackalloc: scrivere C# ad alte prestazioni senza GC overhead non è più un sogno, ma una pratica concreta.

In questo articolo vedremo:

  • differenze tra tipi di dati che creano copie in memoria e tipi che lavorano su riferimenti,
  • esempi reali con confronto versione tradizionale vs versione ottimizzata,
  • scenari applicativi concreti,
  • best practice per usare questi strumenti senza rischi.

Tipi di dati e copie in memoria

Per capire il valore di Span<T>, Memory<T> e stackalloc è fondamentale distinguere tra tipi che creano copie e tipi che lavorano per riferimento.

  • Value type (int, double, struct senza reference): copiati per valore. Ogni assegnazione crea una nuova copia in memoria.
  • Reference type (class, array, string, object): copiati per riferimento, ma con semantica diversa.

Il caso particolare sono le stringhe:

  • sono reference type, ma immutabili.
  • ogni operazione di slicing, concatenazione o Split() crea nuove stringhe in memoria.
  • con grandi quantità di dati, questo porta a un overhead enorme sul GC.

Ed è qui che entra in gioco Span<T>, che permette di lavorare su segmenti di memoria esistenti senza creare copie.


Span<T>: parsing di file di log

Versione tradizionale

string line = "2025-09-17 INFO User logged in";

// Estrazione con Split()
string[] parts = line.Split(' ');
string timestamp = parts[0]; // nuova stringa allocata
string level = parts[1];     // nuova stringa allocata
  • Allocazioni: array + 2 nuove stringhe.
  • Problema: in un file da milioni di righe, il GC passa più tempo a liberare memoria che a lasciare CPU libera per il parsing.

Versione ottimizzata con Span<T>

ReadOnlySpan<char> line = "2025-09-17 INFO User logged in".AsSpan();

// Nessuna nuova stringa
var timestamp = line[..10];     // slice su memoria esistente
var level = line.Slice(11, 4);  // slice su memoria esistente
  • Allocazioni: zero.
  • I dati non vengono copiati, ma solo referenziati con un “puntatore sicuro”.

Risultato: parsing più veloce, riduzione drastica della pressione sul GC.


Memory<T>: networking asincrono

Versione tradizionale

var buffer = new byte[4096]; // nuova allocazione ogni volta
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
ProcessBuffer(buffer.Take(bytesRead).ToArray()); // altra allocazione
  • Allocazioni: un array a ogni ciclo + ToArray().
  • Problema: in server che gestiscono migliaia di connessioni, questo porta a consumo di memoria e pause GC.

Versione ottimizzata con Memory<T>

Memory<byte> buffer = new byte[4096];

int bytesRead = await stream.ReadAsync(buffer);
ProcessBuffer(buffer.Span[..bytesRead]);
  • Allocazioni: una sola, il buffer viene riutilizzato.
  • Vantaggio: Memory<T> può essere passato ad API asincrone e usato in oggetti persistenti, a differenza di Span<T>.

Risultato: riduzione delle allocazioni, throughput maggiore, GC meno stressato.


stackalloc: crittografia e hashing

Versione tradizionale

byte[] temp = new byte[64]; // heap allocation
using var sha256 = SHA256.Create();
byte[] hash = sha256.ComputeHash(data.Concat(temp).ToArray());
  • Allocazioni: array temporanei.
  • Problema: in loop di calcolo intensivo (es. hashing multiplo) il GC accumula overhead inutile.

Versione ottimizzata con stackalloc

Span<byte> temp = stackalloc byte[64];
using var sha256 = SHA256.Create();
sha256.TryComputeHash(data, temp, out int written);
  • Allocazioni: nessuna (memoria sullo stack).
  • Lo stack viene liberato automaticamente a fine metodo.

Risultato: performance estreme, ideale per scenari critici come sicurezza, dispositivi embedded e real-time.


Scenari reali dove fanno la differenza

Ecco una serie di scenari dove avere un codice C# ad alte prestazioni senza GC overhead fa la differenza:

  1. Parsing massivo di dati testuali
    • Differenza tra Split() (allocazioni multiple) e Span<T> (slicing senza copie).
  2. Networking ad alta frequenza
    • Usare Memory<T> al posto di array sempre nuovi: meno frammentazione, prestazioni costanti.
  3. Elaborazione immagini/audio
    • Operare su buffer binari senza duplicare dati temporanei.
  4. Finanza e trading real-time
    • Eliminare pause GC in applicazioni che richiedono latenze prevedibili.
  5. Machine learning
    • Span<float> + SIMD per manipolare array numerici senza copie intermedie.

Best practice

  • Usa Span<T> per slicing e operazioni veloci in metodi sincroni.
  • Usa Memory<T> per buffer asincroni e scenari a lungo termine.
  • Usa stackalloc solo per buffer piccoli (<1KB) per non saturare lo stack.
  • Combina con ArrayPool<T> o MemoryPool<T> per riutilizzare buffer e ridurre allocazioni.
  • Non sacrificare la leggibilità: questi strumenti vanno usati dove servono, non ovunque.

FAQ

1. Qual è la differenza tra tipi che creano copie e tipi che non lo fanno?
Le stringhe, essendo immutabili, creano sempre nuove istanze. Span invece lavora come vista sulla memoria già esistente, evitando copie.

2. Perché Span<T> è più veloce di Split()?
Perché non crea array o stringhe nuove: fa solo slicing su memoria esistente.

3. Posso sostituire sempre array e stringhe con Span<T>?
No, Span non possiede i dati. È un wrapper temporaneo: utile per performance, non per sostituire strutture dati permanenti.

4. stackalloc è sicuro?
Sì, se usato con buffer piccoli. Allocare grandi quantità sullo stack può causare stack overflow.

5. In quali scenari Memory<T> è obbligatorio?
In tutti i contesti asincroni o quando serve conservare un riferimento a lungo termine a una porzione di memoria.


Link esterni di approfondimento

Hai bisogno di una consulenza sullo sviluppo di applicazioni in C# contattami a questo link.


Pubblicato

in

da

Tag: