Partager via


Réduire les allocations de mémoire à l’aide de nouvelles fonctionnalités C#

Importante

Les techniques décrites dans cette section améliorent les performances lorsqu’elles sont appliquées aux chemins d’accès à chaud dans votre code. Les chemins critiques sont ces sections de votre base de code qui sont exécutées souvent et répétées lors des opérations normales. L’application de ces techniques au code qui n’est pas souvent exécuté aura un impact minimal. Avant d’apporter des modifications pour améliorer les performances, il est essentiel de mesurer une base de référence. Ensuite, analysez cette ligne de base pour déterminer où se produisent les goulots d’étranglement de la mémoire. Vous pouvez en savoir plus sur de nombreux outils multiplateformes pour mesurer les performances de votre application dans la section diagnostics et instrumentation. Vous pouvez pratiquer une session de profilage dans le tutoriel pour mesurer l’utilisation de la mémoire dans la documentation Visual Studio.

Une fois que vous avez mesuré l’utilisation de la mémoire et que vous avez déterminé que vous pouvez réduire les allocations, utilisez les techniques de cette section pour réduire les allocations. Après chaque modification successive, mesurez à nouveau l’utilisation de la mémoire. Vérifiez que chaque modification a un impact positif sur l’utilisation de la mémoire dans votre application.

Le travail de performances dans .NET signifie souvent supprimer les allocations de votre code. Chaque bloc de mémoire que vous allouez doit en fin de compte être libéré. Moins d’allocations réduisent le temps passé dans le garbage collection. Il permet un temps d’exécution plus prévisible en supprimant les garbage collections à partir de chemins de code spécifiques.

Une tactique courante pour réduire les allocations consiste à modifier les structures de données critiques de types class en types struct. Cette modification a un impact sur la sémantique de l’utilisation de ces types. Les paramètres et les retours sont désormais passés par valeur au lieu de référence. Le coût de la copie d’une valeur est négligeable si les types sont petits, trois mots ou moins (compte tenu d’un mot de taille naturelle d’un entier). Il est mesurable et peut avoir un véritable impact sur les performances pour des types plus larges. Pour lutter contre l’effet de la copie, les développeurs peuvent passer ces types par ref pour retrouver la sémantique prévue.

Les fonctionnalités C# ref vous permettent d’exprimer la sémantique souhaitée pour struct les types sans avoir un impact négatif sur leur facilité d’utilisation globale. Avant ces améliorations, les développeurs devaient recourir à unsafe des constructions avec des pointeurs et de la mémoire brute pour atteindre le même impact sur les performances. Le compilateur génère du code vérifiable sécurisé pour les nouvelles ref fonctionnalités associées. Le code vérifiable sécurisé signifie que le compilateur détecte les dépassements de mémoire tampon possibles ou l’accès à la mémoire non allouée ou libérée. Le compilateur détecte et empêche certaines erreurs.

Passer et retourner par référence

Les variables en C# stockent des valeurs. Dans struct les types, la valeur est le contenu d’une instance du type. Dans class les types, la valeur est une référence à un bloc de mémoire qui stocke une instance du type. L’ajout du modificateur ref signifie que la variable stocke la référence à la valeur. Dans struct les types, les références pointent vers le stockage contenant la valeur. Dans les types class, les références pointent vers le stockage contenant la référence au bloc de mémoire.

En C#, les paramètres aux méthodes sont passés par valeur et les valeurs de retour sont retournées par valeur. La valeur de l’argument est passée à la méthode. La valeur de l’argument de retour est la valeur de retour.

Le ref, in, ref readonly ou out modificateur indique que l'argument est passé par référence. Une référence au lieu de stockage est transmise à la méthode. L’ajout ref à la signature de méthode signifie que la valeur de retour est retournée par référence. Une référence à l’emplacement de stockage est la valeur de retour.

Vous pouvez également utiliser l’attribution ref pour qu’une variable fasse référence à une autre variable. Une affectation classique copie la valeur du côté droit vers la variable du côté gauche de l’affectation. Une affectation par référence copie l’emplacement de mémoire de la variable du côté droit vers la variable du côté gauche. La ref fait maintenant référence à la variable d'origine.

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

Lorsque vous attribuez une variable, vous modifiez sa valeur. Lorsque vous assignez par référence une variable, vous changez la référence qu'elle désigne.

Vous pouvez travailler directement avec le stockage des valeurs à l’aide de variables ref, passer par référence et assignation par référence. Les règles de portée appliquées par le compilateur garantissent la sécurité lors du travail direct avec le stockage.

Les modificateurs ref readonly et in indiquent tous deux que l’argument doit être passé par référence et ne peut pas être réaffecté dans la méthode. La différence est que ref readonly la méthode utilise le paramètre comme variable. La méthode peut capturer le paramètre ou renvoyer le paramètre par référence en lecture seule. Dans ces cas, vous devez utiliser le ref readonly modificateur. Sinon, le in modificateur offre plus de flexibilité. Vous n’avez pas besoin d’ajouter le in modificateur à un argument pour un in paramètre. Vous pouvez donc mettre à jour les signatures API existantes en toute sécurité à l’aide du in modificateur. Le compilateur émet un avertissement si vous n’ajoutez pas le modificateur ref ou in à un argument pour un paramètre ref readonly.

Contexte ref safe

C# inclut des règles pour ref les expressions afin de s’assurer qu’une ref expression n’est plus accessible où le stockage auquel il fait référence n’est plus valide. Prenons l’exemple suivant :

public ref int CantEscape()
{
    int index = 42;
    return ref index; // Error: index's ref safe context is the body of CantEscape
}

Le compilateur signale une erreur, car vous ne pouvez pas retourner une référence à une variable locale à partir d’une méthode. L’appelant ne peut pas accéder au stockage auquel il est fait référence. Le contexte de sécurité ref définit l’étendue dans laquelle une ref expression est sécurisée pour accéder ou modifier. Le tableau suivant répertorie les contextes de sécurité ref pour les types de variables. ref les champs ne peuvent pas être déclarés dans une class ou une non-ref struct. Par conséquent, ces lignes ne se trouvent pas dans la table :

Déclaration contexte ref safe
non ref local bloc où la variable locale est déclarée
paramètre non ref méthode actuelle
ref, ref readonly, in paramètre méthode d'appel
Paramètre out méthode actuelle
class champ méthode d'appel
champ struct non-ref méthode actuelle
ref champ de ref struct méthode d'appel

Une variable peut être ref retournée si son contexte ref safe est la méthode appelante. Si son contexte de sécurité ref est la méthode actuelle ou un bloc, ref le retour est interdit. L’extrait de code suivant montre deux exemples. Un champ membre est accessible à partir de l’étendue appelant une méthode. Par conséquent, le contexte ref safe d’un champ de classe ou de struct est la méthode appelante. Le contexte de sécurité ref pour un paramètre avec les modificateurs ref, ou in est la méthode entière. Les deux peuvent être ref retournés à partir d’une méthode membre :

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

Remarque

Lorsque le modificateur ref readonly ou in est appliqué à un paramètre, ce paramètre peut être retourné par ref readonly, et non par ref.

Le compilateur garantit qu’une référence ne peut pas échapper à son contexte de sécurité ref. Vous pouvez utiliser ref des paramètres, ref returnet ref des variables locales en toute sécurité, car le compilateur détecte si vous avez écrit accidentellement du code dans lequel une ref expression est accessible lorsque son stockage n’est pas valide.

Contextes safe et ref structs

ref struct les types nécessitent davantage de règles pour s’assurer qu’elles peuvent être utilisées en toute sécurité. Un ref struct type peut inclure des ref champs. Cela nécessite l’introduction d’un contexte sûr. Pour la plupart des types, le contexte sécurisé est la méthode appelante. En d’autres termes, une valeur qui n’est pas une ref struct valeur peut toujours être retournée à partir d’une méthode.

De façon informelle, le contexte sûr d’un ref struct est l’étendue dans laquelle tous ses ref champs sont accessibles. En d'autres termes, il s'agit de l'intersection du contexte ref safe de tous ses ref champs. La méthode suivante renvoie un ReadOnlySpan<char> vers un champ membre, de sorte que le contexte sûr soit la méthode :

private string longMessage = "This is a long message";

public ReadOnlySpan<char> Safe()
{
    var span = longMessage.AsSpan();
    return span;
}

En revanche, le code suivant émet une erreur, car le ref field membre du Span<int> fait référence au tableau d'entiers alloué sur la pile. Cela ne peut pas échapper à la méthode :

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

Unifier les types de mémoire

L'introduction de System.Span<T> et System.Memory<T> fournit un modèle unifié pour travailler avec la mémoire. System.ReadOnlySpan<T> et System.ReadOnlyMemory<T> fournissent des versions en lecture seule pour accéder à la mémoire. Ils fournissent toutes une abstraction sur un bloc de mémoire stockant un tableau d’éléments similaires. La différence est que Span<T> et ReadOnlySpan<T> sont des types ref struct, tandis que Memory<T> et ReadOnlyMemory<T> sont des types struct. Les étendues contiennent un ref field. Par conséquent, les instances d’une étendue ne peuvent pas quitter son contexte sûr. Le contexte sûr d’un ref struct est le contexte ref safe de son ref field. L'implémentation de Memory<T> et ReadOnlyMemory<T> supprime cette restriction. Vous utilisez ces types pour accéder directement aux mémoires tampons.

Améliorer les performances avec la sécurité ref

L’utilisation de ces fonctionnalités pour améliorer les performances implique ces tâches :

  • Évitez les allocations : lorsque vous modifiez un type d’un class à un struct, vous modifiez la façon dont il est stocké. Les variables locales sont stockées sur la pile. Les membres sont stockés inline lorsque l’objet conteneur est alloué. Cette modification signifie moins d’allocations et diminue le travail que fait le garbage collector. Il peut également diminuer la pression de la mémoire afin que le collecteur de déchets s’exécute avec moins souvent.
  • Conserver la sémantique de référence : modification d’un type class à une struct modification de la sémantique de passage d’une variable à une méthode. Le code qui a modifié l’état de ses paramètres a besoin de modification. Maintenant que le paramètre est un struct, la méthode modifie une copie de l’objet d’origine. Vous pouvez restaurer la sémantique d’origine en passant ce paramètre en tant que ref paramètre. Après cette modification, la méthode modifie à nouveau l’original struct .
  • Évitez de copier des données : la copie de types plus volumineux struct peut avoir un impact sur les performances dans certains chemins de code. Vous pouvez également ajouter le modificateur ref pour passer des structures de données plus volumineuses aux méthodes par référence plutôt que par valeur.
  • Restreindre les modifications : lorsqu’un struct type est passé par référence, la méthode appelée peut modifier l’état du struct. Vous pouvez remplacer le modificateur ref par les modificateurs ref readonly ou in pour indiquer que l’argument ne peut pas être modifié. Préférez ref readonly quand la méthode capture le paramètre ou le retourne par référence en lecture seule. Vous pouvez également créer des readonly struct types ou struct types avec readonly membres pour offrir davantage de contrôle sur les membres d’un struct pouvant être modifiés.
  • Manipulation directe de la mémoire : certains algorithmes sont plus efficaces lors du traitement des structures de données comme un bloc de mémoire contenant une séquence d’éléments. Les Span types et Memory fournissent un accès sécurisé aux blocs de mémoire.

Aucune de ces techniques ne nécessite unsafe de code. Utilisé avec sagesse, vous pouvez obtenir des caractéristiques de performances à partir du code sécurisé qui était auparavant uniquement possible à l’aide de techniques non sécurisées. Vous pouvez essayer les techniques vous-même dans le tutoriel sur la réduction des allocations de mémoire.