Ridurre le allocazioni di memoria usando nuove funzionalità C#
Importante
Le tecniche descritte in questa sezione migliorano le prestazioni quando applicate ai percorsi critici nel codice. I percorsi critici sono le sezioni della codebase che vengono eseguite spesso e ripetutamente nelle normali operazioni. L'applicazione di queste tecniche al codice che non viene eseguito spesso avrà un impatto minimo. Prima di apportare modifiche per migliorare le prestazioni, è fondamentale misurare una baseline. Analizzare quindi la baseline per determinare dove si verificano colli di bottiglia della memoria. È possibile ottenere informazioni su molti strumenti multipiattaforma per misurare le prestazioni dell'applicazione nella sezione relativa a Diagnostica e strumentazione. È possibile provare a eseguire una sessione di profilatura nell'esercitazione per Misurare l'utilizzo della memoria nella documentazione di Visual Studio.
Dopo aver misurato l'utilizzo della memoria e aver determinato che è possibile ridurre le allocazioni, usare le tecniche descritte in questa sezione per ridurre le allocazioni. Dopo ogni modifica successiva, misurare nuovamente l'utilizzo della memoria. Assicurarsi che ogni modifica abbia un impatto positivo sull'utilizzo della memoria nell'applicazione.
Il lavoro sulle prestazioni in .NET implica spesso la rimozione delle allocazioni dal codice. Ogni blocco di memoria allocato deve essere liberato prima o poi. Un minor numero di allocazioni riduce il tempo dedicato a Garbage Collection. Consente tempi di esecuzione più prevedibili rimuovendo Garbage Collection da percorsi di codice specifici.
Una tattica comune per ridurre le allocazioni consiste nel modificare le strutture di dati critiche dai tipi class
ai tipi struct
. Questa modifica influisce sulla semantica dell'uso di tali tipi. I parametri e i valori restituiti vengono ora passati per valore anziché per riferimento. Il costo della copia di un valore è trascurabile se i tipi sono piccoli, ovvero tre parole o meno (se si considera che le dimensioni naturali di una parola siano pari a un numero intero). È misurabile e può avere un impatto reale sulle prestazioni per i tipi più grandi. Per contrastare l'effetto della copia, gli sviluppatori possono passare questi tipi per ref
per tornare alla semantica desiderata.
Le funzionalità ref
di C# consentono di esprimere la semantica desiderata per i tipi struct
senza influire negativamente sull'usabilità complessiva. Prima di questi miglioramenti, gli sviluppatori dovevano ricorrere a costrutti unsafe
con puntatori e memoria non elaborata per ottenere lo stesso impatto sulle prestazioni. Il compilatore genera codice sicuro verificabile per le nuove funzionalità correlate a ref
. Per codice sicuro verificabile si intende che il compilatore rileva possibili sovraccarichi del buffer o accessi a memoria non allocata o liberata. Il compilatore rileva e impedisce alcuni errori.
Passare e restituire valori per riferimento
Le variabili in C# archiviano valori. Nei tipi struct
il valore è costituito dai contenuti di un'istanza del tipo. Nei tipi class
il valore è un riferimento a un blocco di memoria che archivia un'istanza del tipo. L'aggiunta del modificatore ref
indica che la variabile archivia il riferimento al valore. Nei tipi struct
il riferimento punta alla risorsa di archiviazione contenente il valore. Nei tipi class
il riferimento punta alla risorsa di archiviazione contenente il riferimento al blocco di memoria.
In C# i parametri dei metodi vengono passati per valore e i valori restituiti vengono restituiti per valore. Il valore dell'argomento viene passato al metodo. Il valore dell'argomento restituito è il valore restituito.
Il modificatore ref
, in
, ref readonly
o out
indica che l'argomento è passato per riferimento. Un riferimento alla posizione di archiviazione viene passato al metodo. L'aggiunta di ref
alla firma del metodo indica che il valore restituito viene restituito per riferimento. Un riferimento alla posizione di archiviazione è il valore restituito.
È anche possibile usare ref assignment per fare in modo che una variabile faccia riferimento a un'altra variabile. Un'assegnazione tipica copia il valore del lato destro nella variabile sul lato sinistro dell'assegnazione. Un ref assignment copia la posizione di memoria della variabile sul lato destro nella variabile sul lato sinistro. ref
fa ora riferimento alla variabile originale:
int anInteger = 42; // assignment.
ref int location = ref anInteger; // ref assignment.
ref int sameLocation = ref location; // ref assignment
Console.WriteLine(location); // output: 42
sameLocation = 19; // assignment
Console.WriteLine(anInteger); // output: 19
Quando usa assign per una variabile, si modifica il rispettivo valore. Quando si usa ref assign per una variabile, si modifica l'elemento a cui fa riferimento.
È possibile usare direttamente la risorsa di archiviazione per i valori usando variabili ref
, passando valori per riferimento e usando ref assignment. Le regole di ambito applicate dal compilatore garantiscono la sicurezza quando si lavora direttamente con la risorsa di archiviazione.
I modificatori ref readonly
e in
indicano entrambi che l'argomento deve essere passato per riferimento e non può essere riassegnato nel metodo. La differenza è che ref readonly
indica che il metodo usa il parametro come variabile. Il metodo potrebbe acquisire il parametro oppure restituire il parametro tramite riferimento di sola lettura. In questi casi è consigliabile usare il modificatore ref readonly
. In caso contrario, il modificatore in
offre maggiore flessibilità. Non è necessario aggiungere il modificatore in
a un argomento per un parametro in
, quindi è possibile aggiornare le firme API esistenti in modo sicuro usando il modificatore in
. Il compilatore genera un avviso se non si aggiunge il modificatore ref
o in
a un argomento per un parametro ref readonly
.
Contesto ref safe
C# include regole per le espressioni ref
per garantire che non sia possibile accedere a un'espressione ref
in cui la risorsa di archiviazione a cui si fa riferimento non è più valida. Si consideri l'esempio seguente:
public ref int CantEscape()
{
int index = 42;
return ref index; // Error: index's ref safe context is the body of CantEscape
}
Il compilatore segnala un errore perché non è possibile restituire un riferimento a una variabile locale da un metodo. Il chiamante non può accedere alla risorsa di archiviazione a cui viene fatto riferimento. Il contesto ref safe definisce l'ambito in cui è possibile accedere o modificare un'espressione ref
in modo sicuro. Nella tabella seguente sono elencati i contesti ref safe per i tipi di variabili. I campi ref
non possono essere dichiarati in una class
o in un struct
non ref, quindi tali righe non sono presenti nella tabella:
Dichiarazione | Contesto ref safe |
---|---|
Locale non ref | Blocco in cui è dichiarato il valore locale |
Parametro non ref | Metodo corrente |
Parametro ref , ref readonly , in |
Metodo chiamante |
parametro out |
Metodo corrente |
Campo diclass |
Metodo chiamante |
Campo struct non ref |
Metodo corrente |
Camporef di ref struct |
Metodo chiamante |
Una variabile può essere restituita per ref
se il relativo contesto ref safe è il metodo chiamante. Se il relativo contesto ref safe è il metodo corrente o un blocco, la restituzione con ref
non è consentita. Il frammento di codice seguente mostra due esempi. È possibile accedere a un campo membro dall'ambito che chiama un metodo, quindi il contesto ref safe di un campo di classe o struct è il metodo chiamante. Il contesto ref safe per un parametro con i modificatori ref
o in
è l'intero metodo. Entrambi possono essere restituiti per ref
da un metodo membro:
private int anIndex;
public ref int RetrieveIndexRef()
{
return ref anIndex;
}
public ref int RefMin(ref int left, ref int right)
{
if (left < right)
return ref left;
else
return ref right;
}
Nota
Quando il modificatore ref readonly
o in
viene applicato a un parametro, tale parametro può essere restituito per ref readonly
, non ref
.
Il compilatore garantisce che un riferimento non possa sfuggire dal rispettivo contesto ref safe. È possibile usare i parametri ref
, ref return
e le variabili locali ref
in modo sicuro perché il compilatore rileva se è stato scritto accidentalmente codice in cui è possibile accedere a un'espressione ref
quando la risorsa di archiviazione non è valida.
Contesto sicuro e struct ref
I tipi ref struct
richiedono più regole per garantire che possano essere usati in modo sicuro. Un tipo ref struct
può includere campi ref
. Ciò richiede l'introduzione di un contesto sicuro. Per la maggior parte dei tipi, il contesto sicuro è il metodo chiamante. In altre parole, un valore che non è ref struct
può sempre essere restituito da un metodo.
In modo informale, il contesto sicuro per un ref struct
è l'ambito in cui è possibile accedere a tutti i relativi campi ref
. In altre parole, è l'intersezione del contesto ref safe di tutti i relativi campi ref
. Il metodo seguente restituisce un ReadOnlySpan<char>
a un campo membro, pertanto il rispettivo contesto sicuro è il metodo:
private string longMessage = "This is a long message";
public ReadOnlySpan<char> Safe()
{
var span = longMessage.AsSpan();
return span;
}
Al contrario, il codice seguente genera un errore perché il membro ref field
di Span<int>
fa riferimento alla matrice di numeri interi allocata dello stack. Non è possibile eseguire l'escape del metodo:
public Span<int> M()
{
int length = 3;
Span<int> numbers = stackalloc int[length];
for (var i = 0; i < length; i++)
{
numbers[i] = i;
}
return numbers; // Error! numbers can't escape this method.
}
Unificare i tipi di memoria
L'introduzione di System.Span<T> e System.Memory<T> fornisce un modello unificato per l'uso della memoria. System.ReadOnlySpan<T> e System.ReadOnlyMemory<T> forniscono versioni di sola lettura per l'accesso alla memoria. Forniscono tutti un'astrazione su un blocco di memoria che archivia una matrice di elementi simili. La differenza è che Span<T>
e ReadOnlySpan<T>
sono tipi ref struct
, mentre Memory<T>
e ReadOnlyMemory<T>
sono tipi struct
. Gli intervalli contengono un ref field
. Le istanze di un intervallo non possono quindi lasciare il relativo contesto sicuro. Il contesto sicuro di un ref struct
è il contesto ref safe del relativo ref field
. L'implementazione di Memory<T>
e ReadOnlyMemory<T>
rimuove questa restrizione. Questi tipi vengono usati per accedere direttamente ai buffer di memoria.
Migliorare le prestazioni con la sicurezza dei riferimenti
L'uso di queste funzionalità per migliorare le prestazioni comporta l'esecuzione di queste attività:
- Evitare allocazioni: quando si modifica un tipo da una
class
a unstruct
, si modifica la modalità di archiviazione. Le variabili locali vengono archiviate nello stack. I membri vengono archiviati inline quando l'oggetto contenitore viene allocato. Questa modifica implica un minor numero di allocazioni e ciò riduce il lavoro svolto dal Garbage Collector. Potrebbe anche diminuire la pressione sulla memoria in modo che il Garbage Collector venga eseguito meno spesso. - Mantenere la semantica di riferimento: la modifica di un tipo da una
class
a unstruct
modifica la semantica del passaggio di una variabile a un metodo. Il codice che ha modificato lo stato dei rispettivi parametri deve essere modificato. Ora che il parametro è unstruct
, il metodo sta modificando una copia dell'oggetto originale. È possibile ripristinare la semantica originale passando tale parametro come parametroref
. Dopo tale modifica, il metodo modifica nuovamente lostruct
originale. - Evitare di copiare i dati: la copia di tipi
struct
di dimensioni maggiori può influire sulle prestazioni in alcuni percorsi di codice. È anche possibile aggiungere il modificatoreref
per passare strutture di dati di dimensioni maggiori ai metodi per riferimento anziché per valore. - Limitare le modifiche: quando un tipo
struct
viene passato per riferimento, il metodo chiamato potrebbe modificare lo stato dello struct. È possibile sostituire il modificatoreref
con i modificatoriref readonly
oin
per indicare che l'argomento non può essere modificato. Usare preferibilmenteref readonly
quando il metodo acquisisce il parametro o lo restituisce tramite riferimento di sola lettura. È anche possibile creare tipireadonly struct
ostruct
con membrireadonly
per fornire maggiore controllo sui membri di unstruct
che possono essere modificati. - Modificare direttamente la memoria: alcuni algoritmi risultano più efficienti quando si considerano le strutture di dati come un blocco di memoria contenente una sequenza di elementi. I tipi
Span
eMemory
forniscono l'accesso sicuro ai blocchi di memoria.
Nessuna di queste tecniche richiede codice unsafe
. Se queste tecniche vengono usate in modo ottimale, è possibile ottenere dal codice sicuro caratteristiche di prestazioni che in precedenza erano possibili solo con tecniche non sicure. È possibile provare le tecniche nell'esercitazione sulla Riduzione delle allocazioni di memoria.