Partager via


Tutoriel : Réduire les allocations de mémoire avec ref sécurité

Souvent, le réglage des performances pour une application .NET implique deux techniques. Tout d’abord, réduisez le nombre et la taille des allocations de tas. Ensuite, réduisez la fréquence à laquelle les données sont copiées. Visual Studio fournit d’excellents outils qui permettent d’analyser la façon dont votre application utilise la mémoire. Une fois que vous avez déterminé où votre application effectue des allocations inutiles, vous apportez des modifications pour réduire ces allocations. Vous convertissez les types class en types struct. Vous utilisez refdes fonctionnalités de sécurité pour préserver la sémantique et réduire la copie supplémentaire.

Utilisez Visual Studio 17.5 pour une expérience optimale avec ce didacticiel. L’outil d’allocation d’objets .NET utilisé pour analyser l’utilisation de la mémoire fait partie de Visual Studio. Vous pouvez utiliser Visual Studio Code et la ligne de commande pour exécuter l’application et apporter toutes les modifications. Toutefois, vous ne pourrez pas voir les résultats d’analyse de vos modifications.

L’application que vous allez utiliser est une simulation d’une application IoT qui surveille plusieurs capteurs pour déterminer si un intrus a entré une galerie secrète avec des objets de valeur. Les capteurs IoT envoient constamment des données qui mesurent la combinaison d’oxygène (O2) et de dioxyde de carbone (CO2) dans l’air. Ils signalent également la température et l’humidité relative. Chacune de ces valeurs varie légèrement tout le temps. Toutefois, lorsqu’une personne entre dans la pièce, le changement un peu plus, et toujours dans la même direction : l’oxygène diminue, le dioxyde de carbone augmente, la température augmente, comme l’humidité relative. Lorsque les capteurs combinent pour afficher des augmentations, l’alarme d’intrus est déclenchée.

Dans ce tutoriel, vous allez exécuter l’application, prendre des mesures sur les allocations de mémoire, puis améliorer les performances en réduisant le nombre d’allocations. Le code source est disponible dans le navigateur d’exemples.

Explorer l’application de démarrage

Téléchargez l’application et exécutez l’exemple de démarrage. L’application de démarrage fonctionne correctement, mais parce qu’elle alloue de nombreux petits objets avec chaque cycle de mesure, ses performances se dégradent lentement au fil du temps.

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

De nombreuses lignes ont été supprimées.

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

Vous pouvez explorer le code pour découvrir le fonctionnement de l’application. Le programme principal exécute la simulation. Une fois que vous appuyez <Enter>sur , il crée une salle et collecte des données de base initiales :

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;
    });

Une fois que les données de référence ont été établies, elles exécutent la simulation sur la salle, où un générateur de nombres aléatoires détermine si un intrus est entré dans la salle :

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;
    });

D’autres types contiennent les mesures, une mesure de réponse qui est la moyenne des 50 dernières mesures et la moyenne de toutes les mesures prises.

Ensuite, exécutez l’application à l’aide de l’outil d’allocation d’objets .NET. Vérifiez que vous utilisez la Release build, et non la Debug build. Dans le menu Débogage , ouvrez le profileur de performances. Cochez l’option .NET Object Allocation Tracking , mais rien d’autre. Exécutez votre application pour terminer. Le profileur mesure les allocations d’objets et signale les allocations et les cycles de nettoyage de la mémoire. Vous devez voir un graphique similaire à l’image suivante :

Graphique d’allocation pour l’exécution de l’application d’alerte d’intrus avant toutes les optimisations.

Le graphique précédent montre que le travail de réduction des allocations offre des avantages en matière de performances. Vous voyez un modèle en dents de scie dans le graphique des objets en temps réel. Cela vous indique que de nombreux objets sont créés qui deviennent rapidement garbage. Ils sont collectés ultérieurement, comme le montre le graphe delta de l'objet. Les barres rouges orientées vers le bas montrent un cycle de ramasse-miettes.

Examinez ensuite l’onglet Allocations sous les graphiques. Ce tableau indique quels types sont alloués le plus :

Graphique montrant quels types sont alloués le plus fréquemment.

Le type System.String représente la plupart des allocations. La tâche la plus importante doit être de réduire la fréquence des allocations de chaînes. Cette application affiche constamment de nombreux affichages formatés sur la console. Pour cette simulation, nous voulons conserver les messages. Nous allons donc nous concentrer sur les deux lignes suivantes : le SensorMeasurement type et le IntruderRisk type.

Double-cliquez sur la SensorMeasurement ligne. Vous pouvez voir que toutes les allocations ont lieu dans la static méthode SensorMeasurement.TakeMeasurement. Vous pouvez voir la méthode dans l’extrait de code suivant :

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
    };
}

Chaque mesure alloue un nouvel SensorMeasurement objet, qui est un class type. Chaque SensorMeasurement créé provoque une allocation de tas.

Modifier les classes en structs

Le code suivant montre la déclaration initiale de 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}
            """;
}

Le type a été créé à l’origine en tant que class parce qu’il contient de nombreuses double mesures. Il est plus grand que ce que vous souhaitez copier dans des chemins chauds. Toutefois, cette décision signifiait un grand nombre d’allocations. Remplacez le type d’un class par un struct.

La modification d’une class pour une struct introduit quelques erreurs de compilateur, car le code d’origine utilisait des vérifications de référence null à quelques endroits. La première est dans la DebounceMeasurement classe, dans la AddMeasurement méthode :

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);
}

Le DebounceMeasurement type contient un tableau de 50 mesures. Les lectures d’un capteur sont signalées comme la moyenne des 50 dernières mesures. Cela réduit le bruit dans les lectures. Avant que 50 lectures complètes aient été prises, ces valeurs sont null. Le code vérifie la null référence pour signaler la moyenne correcte au démarrage du système. Après avoir modifié le SensorMeasurement type en struct, vous devez utiliser un autre test. Le type SensorMeasurement inclut un identificateur de pièce string. Vous pouvez donc utiliser ce test à la place :

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

Les trois autres erreurs du compilateur sont toutes dans la méthode qui prend à plusieurs reprises des mesures dans une salle :

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));
}

Dans la méthode de démarrage, la variable locale pour l’objet SensorMeasurement est une référence nullable :

SensorMeasurement? measure = default;

Maintenant que le SensorMeasurement est un struct au lieu d'un class, le nullable est un type de valeur nullable. Vous pouvez modifier la déclaration en type valeur pour corriger les erreurs restantes du compilateur :

SensorMeasurement measure = default;

Maintenant que les erreurs du compilateur ont été traitées, vous devez examiner le code pour vous assurer que la sémantique n’a pas changé. Étant donné que struct les types sont passés par valeur, les modifications apportées aux paramètres de méthode ne sont pas visibles une fois la méthode retournée.

Importante

La modification d’un type d’un class à un struct peut modifier la sémantique de votre programme. Lorsqu’un class type est passé à une méthode, toutes les mutations effectuées dans la méthode sont apportées à l’argument. Lorsqu’un type struct est passé à une méthode, les mutations effectuées dans la méthode portent sur une copie de l’argument. Cela signifie que toute méthode qui modifie ses arguments par conception doit être mise à jour pour utiliser le ref modificateur sur n’importe quel type d’argument que vous avez changé d’un class à un struct.

Le SensorMeasurement type n’inclut aucune méthode qui change d’état, ce qui n’est pas un problème dans cet exemple. Vous pouvez prouver cela en ajoutant le readonly modificateur à la SensorMeasurement structure :

public readonly struct SensorMeasurement

Le compilateur impose la nature readonly de la structure SensorMeasurement. Si votre inspection du code a manqué une méthode qui a modifié l’état, le compilateur vous indiquerait. Votre application se compile toujours sans erreurs, donc ce type est readonly. L’ajout du readonly modificateur lorsque vous modifiez un type d’un class à un struct peut vous aider à trouver des membres qui modifient l’état du struct.

Éviter d’effectuer des copies

Vous avez supprimé un grand nombre d’allocations inutiles de votre application. Le SensorMeasurement type n’apparaît pas dans le tableau n’importe où.

À présent, il effectue une copie supplémentaire de la SensorMeasurement structure chaque fois qu’elle est utilisée comme paramètre ou valeur de retour. Le SensorMeasurement struct contient quatre doubles, a DateTime et a string. Cette structure est sensiblement plus grande qu’une référence. Ajoutons les modificateurs ref ou in aux emplacements où le type SensorMeasurement est utilisé.

L’étape suivante consiste à rechercher des méthodes qui retournent une mesure ou à prendre une mesure en tant qu’argument et à utiliser des références dans la mesure du possible. Commencez dans la structure SensorMeasurement. La méthode statique TakeMeasurement crée et retourne un nouveau SensorMeasurement:

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
    };
}

Laissons celui-ci en l’état, en retournant par valeur. Si vous avez essayé de retourner par ref, vous obtiendriez une erreur du compilateur. Vous ne pouvez pas retourner ref à une structure nouvellement créée localement dans la méthode. La conception de la structure immuable signifie que vous pouvez uniquement définir les valeurs de la mesure lors de sa création. Cette méthode doit créer une nouvelle structure de mesure.

Examinons à nouveau DebounceMeasurement.AddMeasurement. Vous devez ajouter le in modificateur au measurement paramètre :

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);
}

Cela économise une opération de copie. Le in paramètre est une référence à la copie déjà créée par l’appelant. Vous pouvez également enregistrer une copie avec la TakeMeasurement méthode dans le Room type. Cette méthode illustre la façon dont le compilateur assure la sécurité lorsque vous passez des arguments par ref. La méthode initiale TakeMeasurement dans le Room type prend un argument de Func<SensorMeasurement, bool>. Si vous essayez d’ajouter le modificateur in ou ref à cette déclaration, le compilateur signale une erreur. Vous ne pouvez pas passer d’argument ref à une expression lambda. Le compilateur ne peut pas garantir que l’expression appelée ne copie pas la référence. Si l’expression lambda capture la référence, la référence peut avoir une durée de vie plus longue que la valeur à laquelle elle fait référence. L’accès à celui-ci en dehors de son contexte de sécurité ref entraîne une altération de la mémoire. Les ref règles de sécurité ne l’autorisent pas. Vous pouvez en savoir plus dans la vue d’ensemble des fonctionnalités de sécurité ref.

Conserver la sémantique

Les derniers ensembles de modifications n’ont pas d’impact majeur sur les performances de cette application, car les types ne sont pas créés dans les chemins chauds. Ces modifications illustrent certaines des autres techniques que vous utiliseriez dans votre réglage des performances. Examinons la classe initiale 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));
    }
}

Ce type contient plusieurs propriétés. Certaines sont des types class. La création d’un Room objet implique plusieurs allocations. Un pour le Room lui-même, et un pour chacun des membres d’un class type qu’il contient. Vous pouvez convertir deux de ces propriétés de types class en types struct : les types DebounceMeasurement et AverageMeasurement. Examinons cette transformation avec les deux types.

Remplacez le type DebounceMeasurement de class par struct. Cela introduit une erreur CS8983: A 'struct' with field initializers must include an explicitly declared constructordu compilateur . Vous pouvez résoudre ce problème en ajoutant un constructeur sans paramètre vide :

public DebounceMeasurement() { }

Vous pouvez en savoir plus sur cette exigence dans l’article de référence du langage sur les structures.

Le Object.ToString() remplacement ne modifie aucune des valeurs du struct. Vous pouvez ajouter le readonly modificateur à cette déclaration de méthode. Le DebounceMeasurement type est mutable, vous devez donc vous assurer que les modifications n’affectent pas les copies qui sont supprimées. La AddMeasurement méthode modifie l’état de l’objet. Elle est appelée à partir de la Room classe, dans la TakeMeasurements méthode. Vous souhaitez que ces modifications persistent après l’appel de la méthode. Vous pouvez modifier la Room.Debounce propriété pour renvoyer une référence à une seule instance du DebounceMeasurement type :

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

Il existe quelques modifications dans l’exemple précédent. Tout d’abord, la propriété est une propriété en lecture seule qui retourne une référence en lecture seule à l’instance appartenant à cette pièce. Il est maintenant soutenu par un champ déclaré qui est initialisé lorsque l’objet Room est instancié. Après avoir apporté ces modifications, vous allez mettre à jour l’implémentation de la méthode AddMeasurement. Il utilise le champ de stockage privé, debounceet non la propriété Debounceen lecture seule . Ainsi, les modifications se produisent sur l’instance unique créée lors de l’initialisation.

La même technique fonctionne avec la Average propriété. Tout d’abord, vous modifiez le AverageMeasurement type d’un class à un struct, puis ajoutez le readonly modificateur à la ToString méthode :

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}
        """;
}

Ensuite, vous modifiez la Room classe en suivant la même technique que celle que vous avez utilisée pour la Debounce propriété. La propriété Average retourne un readonly ref au champ privé pour la mesure moyenne. La AddMeasurement méthode modifie les champs internes.

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

Éviter la boxe

Il y a un changement final pour améliorer les performances. Le programme principal imprime des statistiques pour la pièce, y compris l’évaluation des risques :

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

L’appel au ToString généré boxe la valeur d’énumération. Vous pouvez éviter cela en écrivant un remplacement dans la Room classe qui met en forme la chaîne en fonction de la valeur du risque estimé :

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()}";

Ensuite, modifiez le code dans le programme principal pour appeler cette nouvelle ToString méthode :

Console.WriteLine(room.ToString());

Exécutez l’application à l’aide du profileur et examinez la table mise à jour pour les allocations.

Graphique d’allocation pour l’exécution de l’application d’alerte d’intrus après des modifications.

Vous avez supprimé de nombreuses allocations et fourni à votre application une amélioration des performances.

Utilisation de la sécurité de référence dans votre application

Ces techniques sont une optimisation des performances à un niveau bas. Ils peuvent augmenter les performances de votre application lorsqu’elles sont appliquées à des chemins d’accès chauds, et lorsque vous avez mesuré l’impact avant et après les modifications. Dans la plupart des cas, le cycle que vous allez suivre est le suivant :

  • Mesurer les allocations : déterminez les types qui sont le plus alloués et quand vous pouvez réduire les allocations de tas.
  • Convertir la classe en struct : plusieurs fois, les types peuvent être convertis d’un class à un struct. Votre application utilise de l’espace de pile au lieu d’effectuer des allocations de tas.
  • Conserver la sémantique : la conversion d’un class en un struct peut avoir un impact sur la sémantique des paramètres et des valeurs de retour. Toute méthode qui modifie ses paramètres doit maintenant marquer ces paramètres avec le ref modificateur. Cela garantit que les modifications sont apportées à l’objet correct. De même, si une valeur de retour de propriété ou de méthode doit être modifiée par l’appelant, ce retour doit être marqué avec le ref modificateur.
  • Évitez les copies : lorsque vous passez un struct volumineux en tant que paramètre, vous pouvez marquer le paramètre avec le in modificateur. Vous pouvez passer une référence en moins d’octets et vous assurer que la méthode ne modifie pas la valeur d’origine. Vous pouvez également retourner des valeurs en renvoyant readonly ref une référence qui ne peut pas être modifiée.

Ces techniques vous permettent d’améliorer les performances dans les parties critiques de votre code.