Addio byte[]: allocazioni a costo zero in .NET Framework con ReadOnlySpan
#tech
spcnet.it/addio-byte-allocazio…
@informatica
Addio byte[]: allocazioni a costo zero in .NET Framework con ReadOnlySpan
Uno dei pattern di ottimizzazione più semplici e meno conosciuti nel mondo .NET è la sostituzione dei campistatic readonly byte[]con proprietàstatic ReadOnlySpan<byte>. Andrew Lock, noto per le sue analisi approfondite su ASP.NET Core e il runtime, ha pubblicato un articolo che conferma un dettaglio fondamentale: questa tecnica funziona anche su .NET Framework, basta il pacchetto NuGetSystem.Memory. Zero allocazioni, zero costo di startup, nessuna pressione sul garbage collector.Il problema: allocazioni “gratuite” che non lo sono
Consideriamo un pattern che troviamo in quasi tutte le librerie che manipolano dati binari: signature di file, magic number, header fissi, tabelle di lookup. Tipicamente si scrive:public static class MyStaticData { private static readonly byte[] ByteField = new byte[] { 1, 2, 3, 4 }; }
Sembra innocuo: un singolo array, allocato una volta sola al caricamento del tipo. Ma in un processo con migliaia di tipi simili — pensiamo a un parser di formati immagine, a una libreria di crittografia, a un framework web — queste allocazioni si sommano. Ogni array è un oggetto gestito: richiede header, richiede tracciamento GC, occupa spazio sulla Gen 2 (perché sopravvive per sempre) e aumenta i tempi di startup.La soluzione: ReadOnlySpan<byte> come proprietà
La trasformazione è quasi meccanica:public static class MyStaticData { private static ReadOnlySpan<byte> ReadOnlySpanProp => new byte[] { 1, 2, 3, 4 }; }
Sintatticamente sembra che stiamo allocando un array ogni volta che accediamo alla proprietà. In realtà è esattamente il contrario: il compilatore C# riconosce questo pattern e incorpora i byte direttamente nei metadati dell’assembly, costruendo lo span con un puntatore a quei dati. Non viene mai eseguitonewarr.L’IL generato mostra chiaramente la magia:
IL_0000: ldsflda int32 '<PrivateImplementationDetails>'::'...' IL_0005: ldc.i4.4 IL_0006: newobj instance void valuetype [System.Memory]System.ReadOnlySpan`1<unsigned int8>::.ctor(void*, int32) IL_000b: ret
I dati vivono in una sezione di sola lettura dell’assembly; lo span viene costruito on-the-fly con pointer + length. È essenzialmente gratuito.Letterali UTF-8: lo stesso trucco, più ergonomico
A partire da C# 11 (.NET 7), la stessa ottimizzazione si ottiene con i letterali UTF-8:private static ReadOnlySpan<byte> Utf8Hello => "Hello world"u8;
Il suffissou8istruisce il compilatore a codificare la stringa direttamente in UTF-8 nell’assembly. Molto utile per header HTTP, prefissi di protocollo, marker di formato binari — tutti casi in cui storicamente si manteneva unabyte[]statica generata daEncoding.UTF8.GetBytes.I vincoli da rispettare
L’ottimizzazione non si applica in modo uniforme. Vale solo per i tipi a byte singolo:
byte[]sbyte[]bool[]Per gli altri tipi primitivi (int, long, double…) entra in gioco l’endianness: su .NET 7 e successivi c’è
RuntimeHelpers.CreateSpan<T>()che la gestisce in modo trasparente, ma su .NET Framework il compilatore emette codice che cache l’array in un campo statico alla prima chiamata. Ancora efficiente, ma non zero-alloc.Il secondo vincolo è che tutti i valori devono essere costanti a compile-time:
// Anti-pattern: alloca a ogni accesso private static readonly byte One = 1; private static ReadOnlySpan<byte> Bad => new byte[] { One, 2, 3, 4 };
QuiOneè un campo, non una costante, quindi il compilatore deve costruire l’array a runtime. La differenza traconst byteestatic readonly bytediventa improvvisamente importante.Il terzo vincolo è usare
ReadOnlySpan<T>, maiSpan<T>:// Sbagliato: alloca un array mutabile a ogni accesso private static Span<byte> MutSpan => new byte[] { 1, 2, 3, 4 };
UnoSpan<byte>potrebbe essere scritto, e modificare dati immutabili condivisi sarebbe catastrofico. Il compilatore quindi non applica l’ottimizzazione.Il supporto su .NET Framework
Questa è la parte più interessante: il trucco funziona su .NET Framework 4.6.2+ semplicemente referenziando il pacchettoSystem.Memory:<ItemGroup> <PackageReference Include="System.Memory" Version="4.6.3" /> </ItemGroup>
La ragione è che l’ottimizzazione è una feature del compilatore, non del runtime: serve solo cheReadOnlySpan<T>esista come tipo, e il pacchettoSystem.Memorylo fornisce. Chi mantiene librerie multi-target può quindi applicare questa ottimizzazione senza creare codice condizionale#if NET6_0_OR_GREATER.Collection expressions: la rete di sicurezza
Su C# 12 e successivi le collection expressions offrono protezione a compile-time:// Compila e non alloca private static ReadOnlySpan<byte> Safe => [1, 2, 3, 4]; // Errore CS9203 — il compilatore rifiuta private static Span<byte> Dangerous => [1, 2, 3, 4];
L’errore CS9203 è un salvavita: impedisce di assegnare una collection expression a un tipoSpan<T>in contesti static, perché il risultato sarebbe condivisibile e mutabile. Su .NET Framework o su versioni di C# precedenti questa protezione non esiste, quindi serve attenzione in fase di code review.Quando applicarla nel codice reale
Le candidate ideali sono costanti binarie che vivono in campistatic readonly byte[]: magic number (PNG, ZIP, PDF), prefissi protocollari, tabelle di sostituzione, chiavi di test fisse, certificati embedded. Il refactoring è meccanico e non cambia l’API pubblica della classe se la visibility èprivate.Attenzione invece ai metodi che accettano
byte[]: non possiamo passare unoReadOnlySpan<byte>a un’API che richiede un array. In questi casi la scelta è tra riscrivere il consumer per accettareReadOnlySpan<byte>(preferibile) o mantenere l’array tradizionale. Molte API del BCL sono già state aggiornate negli ultimi anni:Stream.Write,HashAlgorithm.ComputeHash,Encoding.GetStringaccettano tuttiReadOnlySpan<byte>in overload moderni.Conclusione
Cambiarestatic readonly byte[]instatic ReadOnlySpan<byte> =>è uno di quei refactoring che riducono allocazioni e startup con una modifica locale a costo zero. Funziona anche su .NET Framework, quindi vale la pena considerarla durante la manutenzione di codice legacy — un punto che spesso sfugge perché l’ecosistema associaSpan<T>esclusivamente a .NET moderno.Fonte: Removing byte[] allocations in .NET Framework using ReadOnlySpan<T> di Andrew Lock.
Removing byte[] allocations in .NET Framework using ReadOnlySpan
In this post I describe how to remove static byte[] allocations, even on .NET Framework, by using Span, look at the associated IL, and discuss the risksAndrew Lock (Andrew Lock | .NET Escapades)
reshared this
The Pirate Post, The Privacy Post, Cybersecurity & cyberwarfare e Poliversity - Università ricerca e giornalismo reshared this.
Marcel Geveler
in reply to netzpolitik.org • • •