Partager via


MemoryOwner<T>

Le MemoryOwner<T> est un type de mémoire tampon implémentant IMemoryOwner<T>, une propriété de longueur incorporée et une série d’API orientées performances. Il s’agit essentiellement d’un wrapper léger autour du type ArrayPool<T>, avec quelques utilitaires d’assistance supplémentaires.

API de plateforme : MemoryOwner<T>, AllocationMode

Fonctionnement

Voici les principales caractéristiques de MemoryOwner<T> :

  • L’un des principaux problèmes des tableaux retournés par les API ArrayPool<T> et des instances de IMemoryOwner<T> retournées par les API MemoryPool<T> est que la taille spécifiée par l’utilisateur est utilisée uniquement en tant que taille minimale : la taille réelle des mémoires tampons retournées peut en réalité être supérieure. MemoryOwner<T> résout cela en stockant également la taille demandée d’origine, de sorte que Memory<T> et les instances Span<T> récupérées à partir de celle-ci n’auront jamais besoin d’être segmentées manuellement.
  • Lors de l’utilisation de IMemoryOwner<T>, l’obtention d’un Span<T> pour la mémoire tampon sous-jacente nécessite d’abord d’obtenir une instance Memory<T> , puis un Span<T>. Cela est assez coûteux, et souvent inutile, car le Memory<T> intermédiaire peut ne pas être nécessaire du tout. Quant à MemoryOwner<T>, il dispose d’une propriété Span supplémentaire qui est extrêmement légère, car elle encapsule directement le tableau interne T[] loué à partir du pool.
  • Les mémoires tampons louées à partir du pool ne sont pas effacées par défaut, ce qui signifie que si elles n’ont pas été effacées lors de leur retour précédent au pool, elles peuvent contenir des données de récupérateur de mémoire. Normalement, les utilisateurs doivent effacer manuellement ces mémoires tampons louées, ce qui peut être indiqué en particulier lorsqu’ils sont effectués fréquemment. MemoryOwner<T> a une approche plus flexible pour cela, par le biais de l’API Allocate(int, AllocationMode). Cette méthode alloue non seulement une nouvelle instance de la taille demandée, mais peut également être utilisée pour spécifier le mode d’allocation à utiliser : soit le même que ArrayPool<T>, soit un autre qui efface automatiquement la mémoire tampon louée.
  • Il existe des cas où une mémoire tampon peut être louée avec une taille supérieure à ce qui est réellement nécessaire, puis redimensionnée par la suite. Cela exigerait normalement que les utilisateurs louent une nouvelle mémoire tampon et copient la région d’intérêt de l’ancienne mémoire tampon. Au lieu de cela, MemoryOwner<T> expose une API Slice(int, int) qui retourne simplement une nouvelle instance encapsulant la zone d’intérêt spécifiée. Cela permet d’ignorer la location d’une nouvelle mémoire tampon et de copier entièrement les éléments.

Syntaxe

Voici un exemple de location d’une mémoire tampon et de récupération d’une instance de Memory<T> :

// Be sure to include this using at the top of the file:
using Microsoft.Toolkit.HighPerformance.Buffers;

using (MemoryOwner<int> buffer = MemoryOwner<int>.Allocate(42))
{
    // Both memory and span have exactly 42 items
    Memory<int> memory = buffer.Memory;
    Span<int> span = buffer.Span;

    // Writing to the span modifies the underlying buffer
    span[0] = 42;
}

Dans cet exemple, nous avons utilisé un bloc using pour déclarer la mémoire tampon MemoryOwner<T> : cela est particulièrement utile, car le tableau sous-jacent est automatiquement retourné au pool à la fin du bloc. Si au lieu de cela, nous n’avons pas de contrôle direct sur la durée de vie d’une instance de MemoryOwner<T>, la mémoire tampon est simplement retournée au pool lorsque l’objet est finalisé par le récupérateur de mémoire. Dans les deux cas, les mémoires tampons louées sont toujours correctement retournées au pool partagé.

Quand cela doit-il être utilisé ?

MemoryOwner<T> peut être utilisé comme type de mémoire tampon à usage général, ce qui offre l’avantage de réduire le nombre d’allocations effectuées au fil du temps, car il réutilise en interne les mêmes tableaux à partir d’un pool partagé. Un cas d’usage courant consiste à remplacer les allocations de tableaux new T[], en particulier lorsque vous effectuez des opérations répétées qui nécessitent une mémoire tampon temporaire pour fonctionner, ou qui produisent une mémoire tampon en conséquence.

Supposons que notre jeu de données est constitué d’une série de fichiers binaires, et que nous devons lire tous ces fichiers et les traiter d’une certaine manière. Pour séparer correctement notre code, nous pouvons finir par écrire une méthode qui lit simplement un fichier binaire, ce qui peut ressembler à cela :

public static byte[] GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    byte[] buffer = new byte[(int)stream.Length];

    stream.Read(buffer, 0, buffer.Length);

    return buffer;
}

Notez cette expression new byte[]. Si nous lisons un grand nombre de fichiers, nous finirons par allouer un grand nombre de nouveaux tableaux, ce qui mettra beaucoup de pression sur le récupérateur de mémoire. Nous souhaitons peut-être refactoriser ce code à l’aide de mémoires tampons louées à partir d’un pool, de la manière suivante :

public static (byte[] Buffer, int Length) GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    byte[] buffer = ArrayPool<T>.Shared.Rent((int)stream.Length);

    stream.Read(buffer, 0, (int)stream.Length);

    return (buffer, (int)stream.Length);
}

À l’aide de cette approche, les mémoires tampons sont désormais louées à partir d’un pool, ce qui signifie que dans la plupart des cas, nous sommes en mesure d’ignorer une allocation. En outre, étant donné que les mémoires tampons louées ne sont pas effacées par défaut, nous pouvons également économiser le temps nécessaire pour les remplir avec des zéros, ce qui offre une autre légère amélioration des performances. Dans l’exemple ci-dessus, le chargement de 1 000 fichiers modifierait la taille totale d’allocation d’environ 1 Mo en seulement 1 024 octets. Une seule mémoire tampon serait allouée, puis réutilisée automatiquement.

Il existe deux problèmes principaux avec le code ci-dessus :

  • ArrayPool<T> peut retourner des mémoires tampons dont la taille est supérieure à celle demandée. Pour contourner ce problème, nous devons retourner un tuple qui indique également la taille réelle utilisée dans notre mémoire tampon louée.
  • En retournant simplement un tableau, nous devons être prudents pour suivre correctement sa durée de vie et le retourner au pool approprié. Nous pouvons contourner ce problème en utilisant MemoryPool<T> à la place et en retournant une instance de IMemoryOwner<T>, mais nous avons toujours le problème de mémoires tampons louées d’une taille supérieure à ce dont nous avons besoin. En outre, IMemoryOwner<T> est surchargé lors de la récupération d’un Span<T> sur lequel travailler, en raison de son caractère d’interface, et nous devons toujours obtenir une instance de Memory<T> d’abord, puis un Span<T>.

Pour résoudre ces deux problèmes, nous pouvons refactoriser ce code à l’aide de MemoryOwner<T> :

public static MemoryOwner<byte> GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    MemoryOwner<byte> buffer = MemoryOwner<byte>.Allocate((int)stream.Length);

    stream.Read(buffer.Span);

    return buffer;
}

L’instance de IMemoryOwner<byte> retournée s’occupe de supprimer la mémoire tampon sous-jacente et de la renvoyer au pool lorsque sa méthode IDisposable.Dispose est appelée. Nous pouvons l’utiliser pour obtenir une instance Memory<T> ou Span<T> pour interagir avec les données chargées, puis supprimer l’instance quand nous n’en avons plus besoin. En outre, toutes les propriétés MemoryOwner<T> (comme MemoryOwner<T>.Span) respectent la taille initiale demandée que nous avons utilisée. Nous n’avons donc plus besoin de suivre manuellement la taille effective dans la mémoire tampon louée.

Exemples

Vous trouverez d’autres exemples dans les tests unitaires.