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 un struct, 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 un struct 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 è un struct, il metodo sta modificando una copia dell'oggetto originale. È possibile ripristinare la semantica originale passando tale parametro come parametro ref. Dopo tale modifica, il metodo modifica nuovamente lo struct 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 modificatore ref 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 modificatore ref con i modificatori ref readonly o in per indicare che l'argomento non può essere modificato. Usare preferibilmente ref readonly quando il metodo acquisisce il parametro o lo restituisce tramite riferimento di sola lettura. È anche possibile creare tipi readonly struct o struct con membri readonly per fornire maggiore controllo sui membri di un struct 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 e Memory 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.