Condividi tramite


Esercitazione: Ridurre le allocazioni di memoria con ref sicurezza

Spesso, l'ottimizzazione delle prestazioni per un'applicazione .NET prevede due tecniche. In primo luogo, ridurre il numero e le dimensioni delle allocazioni dell'heap. In secondo luogo, ridurre la frequenza di copia dei dati. Visual Studio offre ottimi strumenti che consentono di analizzare il modo in cui l'applicazione usa la memoria. Dopo aver determinato dove l'app effettua allocazioni non necessarie, apporta modifiche per ridurre al minimo tali allocazioni. I tipi sono convertiti class in struct tipi. Le ref di sicurezza vengono usate per mantenere la semantica e ridurre al minimo la copia aggiuntiva.

Usare Visual Studio 17.5 per un'esperienza ottimale con questa esercitazione. Lo strumento di allocazione di oggetti .NET usato per analizzare l'utilizzo della memoria fa parte di Visual Studio. È possibile usare Visual Studio Code e la riga di comando per eseguire l'applicazione e apportare tutte le modifiche. Tuttavia, non sarà possibile visualizzare i risultati dell'analisi delle modifiche.

L'applicazione che verrà usata è una simulazione di un'applicazione IoT che monitora diversi sensori per determinare se un intruso è entrato in una raccolta segreta con oggetti di valore. I sensori IoT inviano costantemente dati che misurano la combinazione di ossigeno (O2) e anidride carbonica (CO2) nell'aria. Segnalano anche la temperatura e l'umidità relativa. Ognuno di questi valori varia leggermente. Tuttavia, quando una persona entra nella stanza, il cambiamento un po 'più, e sempre nella stessa direzione: ossigeno diminuisce, anidride carbonica aumenta, la temperatura aumenta, così come l'umidità relativa. Quando i sensori si combinano per mostrare aumenti, viene attivato l'allarme intruso.

In questa esercitazione si eseguirà l'applicazione, si eseguiranno misurazioni sulle allocazioni di memoria, quindi si miglioreranno le prestazioni riducendo il numero di allocazioni. Il codice sorgente è disponibile nel browser degli esempi.

Esplorare l'applicazione iniziale

Scaricare l'applicazione ed eseguire l'esempio di avvio. L'applicazione iniziale funziona correttamente, ma poiché alloca molti oggetti di piccole dimensioni con ogni ciclo di misurazione, le prestazioni diminuiscono lentamente man mano che vengono eseguite nel tempo.

Press <return> to start simulation

Debounced measurements:
    Temp:      67.332
    Humidity:  41.077%
    Oxygen:    21.097%
    CO2 (ppm): 404.906
Average measurements:
    Temp:      67.332
    Humidity:  41.077%
    Oxygen:    21.097%
    CO2 (ppm): 404.906

Debounced measurements:
    Temp:      67.349
    Humidity:  46.605%
    Oxygen:    20.998%
    CO2 (ppm): 408.707
Average measurements:
    Temp:      67.349
    Humidity:  46.605%
    Oxygen:    20.998%
    CO2 (ppm): 408.707

Molte righe rimosse.

Debounced measurements:
    Temp:      67.597
    Humidity:  46.543%
    Oxygen:    19.021%
    CO2 (ppm): 429.149
Average measurements:
    Temp:      67.568
    Humidity:  45.684%
    Oxygen:    19.631%
    CO2 (ppm): 423.498
Current intruders: 3
Calculated intruder risk: High

Debounced measurements:
    Temp:      67.602
    Humidity:  46.835%
    Oxygen:    19.003%
    CO2 (ppm): 429.393
Average measurements:
    Temp:      67.568
    Humidity:  45.684%
    Oxygen:    19.631%
    CO2 (ppm): 423.498
Current intruders: 3
Calculated intruder risk: High

È possibile esplorare il codice per informazioni sul funzionamento dell'applicazione. Il programma principale esegue la simulazione. Dopo aver premuto <Enter>, crea una stanza e raccoglie alcuni dati di base iniziali:

Console.WriteLine("Press <return> to start simulation");
Console.ReadLine();
var room = new Room("gallery");
var r = new Random();

int counter = 0;

room.TakeMeasurements(
    m =>
    {
        Console.WriteLine(room.Debounce);
        Console.WriteLine(room.Average);
        Console.WriteLine();
        counter++;
        return counter < 20000;
    });

Dopo aver stabilito i dati di base, esegue la simulazione nella stanza, dove un generatore di numeri casuali determina se un intruso è entrato nella stanza:

counter = 0;
room.TakeMeasurements(
    m =>
    {
        Console.WriteLine(room.Debounce);
        Console.WriteLine(room.Average);
        room.Intruders += (room.Intruders, r.Next(5)) switch
        {
            ( > 0, 0) => -1,
            ( < 3, 1) => 1,
            _ => 0
        };

        Console.WriteLine($"Current intruders: {room.Intruders}");
        Console.WriteLine($"Calculated intruder risk: {room.RiskStatus}");
        Console.WriteLine();
        counter++;
        return counter < 200000;
    });

Altri tipi contengono le misurazioni, una misura debounced che corrisponde alla media degli ultimi 50 misurazioni e alla media di tutte le misurazioni effettuate.

Eseguire quindi l'applicazione usando lo strumento di allocazione di oggetti .NET. Assicurarsi di usare la Release compilazione, non la Debug compilazione. Nel menu Debug aprire il profiler prestazioni. Selezionare l'opzione Rilevamento dell'allocazione degli oggetti .NET, ma nient'altro. Eseguire l'applicazione fino al completamento. Il profiler misura le allocazioni di oggetti e segnala i cicli di raccolta dei rifiuti. Verrà visualizzato un grafico simile all'immagine seguente:

Grafico di allocazione per l'esecuzione dell'app di avviso intrusa prima di qualsiasi ottimizzazione.

Il grafico precedente mostra che il lavoro per ridurre al minimo le allocazioni offrirà vantaggi in termini di prestazioni. Nel grafico degli oggetti attivi viene visualizzato un modello a denti di sega. Ciò indica che vengono creati numerosi oggetti che diventano rapidamente garbage. I dati vengono raccolti successivamente, come mostrato nel grafico delta dell'oggetto. Le barre rosse in discesa indicano un ciclo di raccolta dei rifiuti.

Esaminare quindi la scheda Allocazioni sotto i grafici. Questa tabella mostra quali tipi vengono allocati di più:

Grafico che mostra i tipi allocati più frequentemente.

Il System.String tipo rappresenta la maggior parte delle allocazioni. L'attività più importante deve essere quella di ridurre al minimo la frequenza delle allocazioni di stringhe. Questa applicazione stampa costantemente numerosi output formattati nella console. Per questa simulazione, vogliamo mantenere i messaggi, quindi ci concentreremo sulle due righe seguenti: tipo SensorMeasurement e tipo IntruderRisk.

Fare doppio clic sulla SensorMeasurement riga. È possibile notare che tutte le allocazioni vengono eseguite nel static metodo SensorMeasurement.TakeMeasurement. È possibile visualizzare il metodo nel frammento di codice seguente:

public static SensorMeasurement TakeMeasurement(string room, int intruders)
{
    return new SensorMeasurement
    {
        CO2 = (CO2Concentration + intruders * 10) + (20 * generator.NextDouble() - 10.0),
        O2 = (O2Concentration - intruders * 0.01) + (0.005 * generator.NextDouble() - 0.0025),
        Temperature = (TemperatureSetting + intruders * 0.05) + (0.5 * generator.NextDouble() - 0.25),
        Humidity = (HumiditySetting + intruders * 0.005) + (0.20 * generator.NextDouble() - 0.10),
        Room = room,
        TimeRecorded = DateTime.Now
    };
}

Ogni misura alloca un nuovo SensorMeasurement oggetto, ovvero un class tipo. Ogni SensorMeasurement creato causa un'allocazione dell'heap.

Modificare le classi in struct strutture

Il codice seguente illustra la dichiarazione iniziale di SensorMeasurement:

public class SensorMeasurement
{
    private static readonly Random generator = new Random();

    public static SensorMeasurement TakeMeasurement(string room, int intruders)
    {
        return new SensorMeasurement
        {
            CO2 = (CO2Concentration + intruders * 10) + (20 * generator.NextDouble() - 10.0),
            O2 = (O2Concentration - intruders * 0.01) + (0.005 * generator.NextDouble() - 0.0025),
            Temperature = (TemperatureSetting + intruders * 0.05) + (0.5 * generator.NextDouble() - 0.25),
            Humidity = (HumiditySetting + intruders * 0.005) + (0.20 * generator.NextDouble() - 0.10),
            Room = room,
            TimeRecorded = DateTime.Now
        };
    }

    private const double CO2Concentration = 409.8; // increases with people.
    private const double O2Concentration = 0.2100; // decreases
    private const double TemperatureSetting = 67.5; // increases
    private const double HumiditySetting = 0.4500; // increases

    public required double CO2 { get; init; }
    public required double O2 { get; init; }
    public required double Temperature { get; init; }
    public required double Humidity { get; init; }
    public required string Room { get; init; }
    public required DateTime TimeRecorded { get; init; }

    public override string ToString() => $"""
            Room: {Room} at {TimeRecorded}:
                Temp:      {Temperature:F3}
                Humidity:  {Humidity:P3}
                Oxygen:    {O2:P3}
                CO2 (ppm): {CO2:F3}
            """;
}

Il tipo è stato originariamente creato come oggetto class perché contiene numerose double misurazioni. È più grande di quanto si voglia copiare nei percorsi ad accesso frequente. Tuttavia, tale decisione ha significato un numero elevato di allocazioni. Modificare il tipo da class a struct.

Modificare da class a struct introduce alcuni errori del compilatore poiché il codice originale utilizzava controlli di riferimento con null in alcuni punti. Il primo è nella classe DebounceMeasurement, nel metodo AddMeasurement:

public void AddMeasurement(SensorMeasurement datum)
{
    int index = totalMeasurements % debounceSize;
    recentMeasurements[index] = datum;
    totalMeasurements++;
    double sumCO2 = 0;
    double sumO2 = 0;
    double sumTemp = 0;
    double sumHumidity = 0;
    for (int i = 0; i < debounceSize; i++)
    {
        if (recentMeasurements[i] is not null)
        {
            sumCO2 += recentMeasurements[i].CO2;
            sumO2+= recentMeasurements[i].O2;
            sumTemp+= recentMeasurements[i].Temperature;
            sumHumidity += recentMeasurements[i].Humidity;
        }
    }
    O2 = sumO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
    CO2 = sumCO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
    Temperature = sumTemp / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
    Humidity = sumHumidity / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
}

Il DebounceMeasurement tipo contiene una matrice di 50 misure. Le letture per un sensore vengono segnalate come media delle ultime 50 misurazioni. Ciò riduce il rumore nelle letture. Prima che siano state acquisite 50 letture complete, questi valori sono null. Il codice verifica il riferimento null per segnalare la media corretta all'avvio del sistema. Dopo aver modificato il SensorMeasurement tipo in uno struct, è necessario usare un test diverso. Il SensorMeasurement tipo include un string per identificare la stanza, quindi puoi usare quel test.

if (recentMeasurements[i].Room is not null)

Gli altri tre errori del compilatore sono tutti nel metodo che esegue ripetutamente le misurazioni in una stanza:

public void TakeMeasurements(Func<SensorMeasurement, bool> MeasurementHandler)
{
    SensorMeasurement? measure = default;
    do {
        measure = SensorMeasurement.TakeMeasurement(Name, Intruders);
        Average.AddMeasurement(measure);
        Debounce.AddMeasurement(measure);
    } while (MeasurementHandler(measure));
}

Nel metodo iniziale, la variabile locale per il SensorMeasurement è un riferimento nullable:

SensorMeasurement? measure = default;

Ora che SensorMeasurement è un struct invece di un class, il nullable è un tipo di valore nullable. È possibile modificare la dichiarazione in un tipo di valore per correggere gli errori rimanenti del compilatore:

SensorMeasurement measure = default;

Ora che gli errori del compilatore sono stati risolti, è necessario esaminare il codice per assicurarsi che la semantica non sia stata modificata. Poiché struct i tipi vengono passati per valore, le modifiche apportate ai parametri del metodo non sono visibili dopo la restituzione del metodo.

Importante

La modifica di un tipo da a class a struct può modificare la semantica del programma. Quando si passa un tipo class a un metodo, qualsiasi mutazione effettuata nel metodo viene applicata all'argomento. Quando un tipo struct viene passato a un metodo, le mutazioni apportate all'interno del metodo vengono eseguite su una copia dell'argomento. Ciò significa che qualsiasi metodo che modifica gli argomenti come previsto deve essere aggiornato per usare il modificatore ref su qualsiasi tipo di argomento che hai modificato da class a struct.

Il SensorMeasurement tipo non include metodi che modificano lo stato, quindi non è un problema in questo esempio. È possibile dimostrare che aggiungendo il readonly modificatore allo SensorMeasurement struct :

public readonly struct SensorMeasurement

Il compilatore applica la readonly natura dello SensorMeasurement struct. Se l'ispezione del codice manca un metodo che modifica lo stato, il compilatore te lo dirà. L'app continua a compilarsi senza errori, quindi questo tipo è readonly. L'aggiunta del modificatore readonly quando si modifica un tipo da class a struct consente di trovare membri che modificano lo stato del struct.

Evitare di eseguire copie

È stato rimosso un numero elevato di allocazioni non necessarie dall'app. Il SensorMeasurement tipo non è presente in alcun punto della tabella.

Ora, esegue operazioni aggiuntive per copiare la SensorMeasurement struttura ogni volta che viene usata come parametro o come valore restituito. Lo SensorMeasurement struct contiene quattro valori double, un DateTime e un oggetto string. Tale struttura è molto più grande di un riferimento. Aggiungiamo i modificatori ref o in ai luoghi dove si utilizza il tipo SensorMeasurement.

Il passaggio successivo consiste nel trovare i metodi che restituiscono una misura o prendere una misura come argomento e usare i riferimenti laddove possibile. Inizia nel struct SensorMeasurement. Il metodo statico TakeMeasurement crea e restituisce un nuovo SensorMeasurementoggetto :

public static SensorMeasurement TakeMeasurement(string room, int intruders)
{
    return new SensorMeasurement
    {
        CO2 = (CO2Concentration + intruders * 10) + (20 * generator.NextDouble() - 10.0),
        O2 = (O2Concentration - intruders * 0.01) + (0.005 * generator.NextDouble() - 0.0025),
        Temperature = (TemperatureSetting + intruders * 0.05) + (0.5 * generator.NextDouble() - 0.25),
        Humidity = (HumiditySetting + intruders * 0.005) + (0.20 * generator.NextDouble() - 0.10),
        Room = room,
        TimeRecorded = DateTime.Now
    };
}

Questa opzione verrà lasciata invariata, restituendo in base al valore. Se si tenta di restituire da ref, si otterrà un errore del compilatore. Non è possibile restituire un oggetto ref a una nuova struttura creata localmente nel metodo . La progettazione dello struct non modificabile significa che è possibile impostare solo i valori della misura in fase di costruzione. Questo metodo deve creare un nuovo struct di misurazione.

Esaminiamo di nuovo DebounceMeasurement.AddMeasurement. È necessario aggiungere il in modificatore al measurement parametro :

public void AddMeasurement(in SensorMeasurement datum)
{
    int index = totalMeasurements % debounceSize;
    recentMeasurements[index] = datum;
    totalMeasurements++;
    double sumCO2 = 0;
    double sumO2 = 0;
    double sumTemp = 0;
    double sumHumidity = 0;
    for (int i = 0; i < debounceSize; i++)
    {
        if (recentMeasurements[i].Room is not null)
        {
            sumCO2 += recentMeasurements[i].CO2;
            sumO2+= recentMeasurements[i].O2;
            sumTemp+= recentMeasurements[i].Temperature;
            sumHumidity += recentMeasurements[i].Humidity;
        }
    }
    O2 = sumO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
    CO2 = sumCO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
    Temperature = sumTemp / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
    Humidity = sumHumidity / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
}

In questo modo viene salvata un'operazione di copia. Il in parametro è un riferimento alla copia già creata dal chiamante. È anche possibile salvare una copia con il TakeMeasurement metodo nel Room tipo . Questo metodo illustra come il compilatore garantisce la sicurezza quando si passano argomenti tramite ref. Il metodo iniziale TakeMeasurement nel tipo Room richiede un argomento di Func<SensorMeasurement, bool>. Se si tenta di aggiungere il in modificatore o ref a tale dichiarazione, il compilatore segnala un errore. Non è possibile passare un ref argomento a un'espressione lambda. Il compilatore non può garantire che l'espressione chiamata non copia il riferimento. Se l'espressione lambda acquisisce il riferimento, il riferimento potrebbe avere una durata superiore al valore a cui fa riferimento. L'accesso all'esterno del contesto di riferimento sicuro comporta un danneggiamento della memoria. Le ref regole di sicurezza non lo consentono. Per altre informazioni, vedere la panoramica delle funzionalità di sicurezza di riferimento.

Mantenere la semantica

I set finali di modifiche non avranno un impatto significativo sulle prestazioni di questa applicazione perché i tipi non vengono creati in percorsi ad accesso frequente. Queste modifiche illustrano alcune delle altre tecniche usate nell'ottimizzazione delle prestazioni. Di seguito viene esaminata la classe iniziale Room :

public class Room
{
    public AverageMeasurement Average { get; } = new ();
    public DebounceMeasurement Debounce { get; } = new ();
    public string Name { get; }

    public IntruderRisk RiskStatus
    {
        get
        {
            var CO2Variance = (Debounce.CO2 - Average.CO2) > 10.0 / 4;
            var O2Variance = (Average.O2 - Debounce.O2) > 0.005 / 4.0;
            var TempVariance = (Debounce.Temperature - Average.Temperature) > 0.05 / 4.0;
            var HumidityVariance = (Debounce.Humidity - Average.Humidity) > 0.20 / 4;
            IntruderRisk risk = IntruderRisk.None;
            if (CO2Variance) { risk++; }
            if (O2Variance) { risk++; }
            if (TempVariance) { risk++; }
            if (HumidityVariance) { risk++; }
            return risk;
        }
    }

    public int Intruders { get; set; }

    
    public Room(string name)
    {
        Name = name;
    }

    public void TakeMeasurements(Func<SensorMeasurement, bool> MeasurementHandler)
    {
        SensorMeasurement? measure = default;
        do {
            measure = SensorMeasurement.TakeMeasurement(Name, Intruders);
            Average.AddMeasurement(measure);
            Debounce.AddMeasurement(measure);
        } while (MeasurementHandler(measure));
    }
}

Questo tipo contiene diverse proprietà. Alcuni tipi sono class. La creazione di un Room oggetto comporta più allocazioni. Uno per il Room stesso e uno per ciascuno dei membri di un tipo class che contiene. È possibile convertire due di queste proprietà da tipi di class a tipi di struct: i tipi di DebounceMeasurement e i tipi di AverageMeasurement. Verrà ora illustrata la trasformazione con entrambi i tipi.

Modificare il DebounceMeasurement tipo da un class a struct. In questo modo viene introdotto un errore CS8983: A 'struct' with field initializers must include an explicitly declared constructordel compilatore . È possibile risolvere questo problema aggiungendo un costruttore vuoto senza parametri:

public DebounceMeasurement() { }

Per saperne di più su questo requisito, consulta l'articolo di riferimento sul linguaggio relativo ai struct.

L'override Object.ToString() non modifica i valori dello struct. È possibile aggiungere il readonly modificatore alla dichiarazione del metodo. Il DebounceMeasurement tipo è modificabile, quindi è necessario prestare attenzione che le modifiche non influiscano sulle copie rimosse. Il AddMeasurement metodo modifica lo stato dell'oggetto. Viene chiamato dalla classe Room, nel metodo TakeMeasurements. Vuoi che quelle modifiche persistano dopo aver chiamato il metodo. È possibile modificare la Room.Debounce proprietà in modo da restituire un riferimento a una singola istanza del DebounceMeasurement tipo:

private DebounceMeasurement debounce = new();
public ref readonly DebounceMeasurement Debounce { get { return ref debounce; } }

Nell'esempio precedente sono state apportate alcune modifiche. Innanzitutto, la proprietà è una proprietà di sola lettura che restituisce un riferimento di sola lettura all'istanza che appartiene a questa stanza. È ora supportato da un campo dichiarato che viene inizializzato quando l'oggetto Room viene istanziato. Dopo aver apportato queste modifiche, si aggiornerà l'implementazione del AddMeasurement metodo . Usa il campo sottostante privato, debounce, non la proprietà readonly Debounce. In questo modo, le modifiche vengono apportate alla singola istanza creata durante l'inizializzazione.

La stessa tecnica funziona con la Average proprietà . Prima di tutto, modificate il tipo AverageMeasurement da class a struct, e aggiungete il modificatore readonly al metodo ToString.

namespace IntruderAlert;

public struct AverageMeasurement
{
    private double sumCO2 = 0;
    private double sumO2 = 0;
    private double sumTemperature = 0;
    private double sumHumidity = 0;
    private int totalMeasurements = 0;

    public AverageMeasurement() { }

    public readonly double CO2 => sumCO2 / totalMeasurements;
    public readonly double O2 => sumO2 / totalMeasurements;
    public readonly double Temperature => sumTemperature / totalMeasurements;
    public readonly double Humidity => sumHumidity / totalMeasurements;

    public void AddMeasurement(in SensorMeasurement datum)
    {
        totalMeasurements++;
        sumCO2 += datum.CO2;
        sumO2 += datum.O2;
        sumTemperature += datum.Temperature;
        sumHumidity+= datum.Humidity;
    }

    public readonly override string ToString() => $"""
        Average measurements:
            Temp:      {Temperature:F3}
            Humidity:  {Humidity:P3}
            Oxygen:    {O2:P3}
            CO2 (ppm): {CO2:F3}
        """;
}

Quindi, si modifica la Room classe seguendo la stessa tecnica usata per la Debounce proprietà . La proprietà Average restituisce un readonly ref al campo privato per la media delle misurazioni. Il AddMeasurement metodo modifica i campi interni.

private AverageMeasurement average = new();
public  ref readonly AverageMeasurement Average { get { return ref average; } }

Evitare il boxing

È stata apportata una modifica finale per migliorare le prestazioni. Il programma principale è la stampa delle statistiche per la stanza, inclusa la valutazione dei rischi:

Console.WriteLine($"Current intruders: {room.Intruders}");
Console.WriteLine($"Calculated intruder risk: {room.RiskStatus}");

La chiamata alle caselle generate ToString incapsula il valore dell'enumerazione. È possibile evitare che scrivendo un override nella Room classe che formatta la stringa in base al valore del rischio stimato:

public override string ToString() =>
    $"Calculated intruder risk: {RiskStatus switch
    {
        IntruderRisk.None => "None",
        IntruderRisk.Low => "Low",
        IntruderRisk.Medium => "Medium",
        IntruderRisk.High => "High",
        IntruderRisk.Extreme => "Extreme",
        _ => "Error!"
    }}, Current intruders: {Intruders.ToString()}";

Modificare quindi il codice nel programma principale per chiamare questo nuovo ToString metodo:

Console.WriteLine(room.ToString());

Esegui l'app usando il profiler ed esamina la tabella aggiornata per vedere le allocazioni.

Grafico di allocazione per l'esecuzione dell'app di avviso intrusa dopo le modifiche.

Hai rimosso numerose allocazioni e hai fornito all'app un miglioramento delle prestazioni.

Uso della sicurezza dei riferimenti nell'applicazione

Queste tecniche sono l'ottimizzazione delle prestazioni di basso livello. Possono aumentare le prestazioni nell'applicazione quando vengono applicati ai percorsi ad accesso frequente e quando si è misurato l'impatto prima e dopo le modifiche. Nella maggior parte dei casi, il ciclo che si seguirà è:

  • Misurare le allocazioni: determinare quali tipi vengono allocati maggiormente e quando è possibile ridurre le allocazioni dell'heap.
  • Convertire la classe in uno struct: molte volte, i tipi possono essere convertiti da un class oggetto a un oggetto struct. L'app usa lo spazio dello stack invece di effettuare allocazioni di heap.
  • Mantieni la semantica: Convertire un class in un struct può influire sulla semantica dei parametri e dei valori restituiti. Qualsiasi metodo che modifica i parametri deve ora contrassegnare tali parametri con il ref modificatore. Ciò garantisce che le modifiche vengano apportate all'oggetto corretto. Analogamente, se un valore restituito di una proprietà o di un metodo deve essere modificato dal chiamante, tale restituzione deve essere contrassegnata con il ref modificatore.
  • Evitare copie: quando si passa uno struct di grandi dimensioni come parametro, è possibile contrassegnare il parametro con il in modificatore. È possibile passare un riferimento in meno byte e assicurarsi che il metodo non modifichi il valore originale. È anche possibile restituire valori in readonly ref base a per restituire un riferimento che non può essere modificato.

Usando queste tecniche è possibile migliorare le prestazioni nei percorsi ad accesso frequente del codice.