Span
Γ¨ un nuovo tipo introdotto in C#7.2 e supportato dal .NET Core 2.1 in poi ed Γ¨ utilizzato per ottenere un puntatore type-safe ad una area contigua di memoria (che sia sullo Heap, Stack o anche unmanaged).
Utilizzando lo Span
Γ¨ possibile effettuare delle elaborazioni su tale oggetto in memoria 100% sullo stack senza passare dallo heap, risparmiando quindi memoria e facendo risparmiare tempo al Garbage Collector.
Eβ importante utilizzarlo in software dove la performance e la gestione della memoria sono importanti.
Esiste anche la versione in sola lettura dello Span
chiamata ReadOnlySpan
utilizzata principalmente per manipolare sullo stack oggetti immutabili come le Le stringhe in C Sharp.
Implementazione
public readonly ref struct Span<T>
{
private readonly ref T \_pointer;
private readonly int \_length;
public ref T this[int index] => ref \_pointer + index;
...
}
Lo Span
Γ¨ un ref struct
che quindi, per definizione, non puΓ² essere allocato sullo heap.
Inoltre per accedere ad un elemento utilizzo il ref return
che permette di ritornare il puntatore allβoggetto ritornato e non un valore.
Per gli oggetti passati per referenza non cambia nulla, mentre cambia per gli oggetti passati per valore e soprattutto per le stringhe.
Funzionamento
Per spiegare il funzionamento utilizzo un ReadOnlySpan
che, come dice il nome, Γ¨ lβanalogo dello Span
ma in sola lettura, quindi senza la possibilitΓ di modificare la memoria. Viene utilizzato principalmente per manipolare stringhe.
Uno ReadOnlySpan
parte sempre da una variabile presente sullo heap (per esempio una stringa giΓ allocata) e ne effettua delle elaborazioni utilizzando solo offset e lunghezze.
Per esempio se ho la stringa βfoo bar
β sullo heap e voglio lavorare solo su βfoo
β posso creare uno ReadOnlySpan
sullo stack con offset 0 e lunghezza 3.
Senza quindi allocare alcuna stringa ulteriore ho una rappresentazione di βfoo
β.
Il risparmio di allocazione porta sia ad un risparmio di memoria ma anche migliora le performance del GC che non deve pulire da stringhe non utilizzate.
Lo ReadOnlySpan
Γ¨ molto piΓΉ limitato sulle cose che puΓ² fare rispetto alle stringhe, perΓ² puΓ² essere utile in alcuni casi specifici dove la performance Γ¨ importante.
Esempio
Per capire meglio il funzionamento facciamo dei test con Benchmark.net. Il problema da risolvere Γ¨ il seguente: data una stringa di due parole separate da spazio prenderne la prima.
Ho pensato a tre implementazioni diverse, dalla piΓΉ lenta alla piΓΉ veloce.
La classe contenente ha una const "TwoWords = "Foo Bar"
; e i seguenti header
[RankColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[MemoryDiagnoser]
public class SpanTests{}
Implementazione 1 - LINQ
[Benchmark]
public string GetFirstString()
{
// Alloco un array di stringhe sullo heap, in particolare alloco "Foo" e "Bar"
var words = TwoWords.Split(" ");
// Prendo l'ultimo valore dell'array. Dato che le stringhe sono immutabili alloco una ulteriore sullo heap
var firstWord = words.FirstOrDefault();
// Ritorno la prima parola. L'array words contenente le due string che vanno out of scope in quanto perdo i puntatori dello stack.
// Essendo variabili locali entreranno nella gen 0 del GC e al prossimo collect verranno eliminate
return firstWord ?? string.Empty;
}
Implementazione 2 - Substring
[Benchmark]
public string GetFirstWordUsingSubstring()
{
// Ottengo un valore intero dell'ultimo index di ' ' (sono sullo stack)
var lastSpaceindex = TwoWords.LastindexOf(" ", StringComparison.Ordinal);
// Il metodo Substring alloca una nuova stringa sullo heap contentente "Foo"
var firstString = TwoWords.Substring(0, lastSpaceindex);
return lastSpaceindex == -1 ? string.Empty : firstString;
}
Implementazione 3 - ReadOnlySpan
[Benchmark]
public ReadOnlySpan<char> GetFirstWordUsingSpanAndLastindexOf()
{
// Creo uno Span sullo stack che punta alla variabile \_twoWords sullo heap
ReadOnlySpan<char> nameAndSurnameAsSpan = TwoWords;
// Ottengo un valore intero dell'ultimo index di ' ' (sono sullo stack)
var lastSpaceindex = nameAndSurnameAsSpan.LastindexOf(' ');
// Utilizzo Slice che Γ¨ l'analogo di Substring. Aggiungo allo stack due variabili: offset "0" e lenght "lastSpaceindex".
var firstString = nameAndSurnameAsSpan.Slice(0, lastSpaceindex);
return lastSpaceindex == -1 ? ReadOnlySpan<char>.Empty : firstString;
}
Risultati
Dopo aver lanciato il benchmark questi sono i risultati:
Il metodo GetFirstString
Γ¨ il piΓΉ lento e inoltre alloca piΓΉ memoria e, dal codice Γ¨ evidente il motivo: ho un nuovo array di stringhe sullo heap e inoltre una chiamata a LINQ che non Γ¨ performante.
Il metodo GetFirstWordUsingSubstring
utilizza Substring
invece di FirstOrDefault
di LINQ e infatti ho un notevole miglioramento di performance e di allocazione di memoria.
Lβultimo metodo GetFirstWordUsingSpanAndLastindexOf
Γ¨ estremamente veloce e soprattutto non porta ad alcuna nuova allocazione di memoria.