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 deIMemoryOwner<T>
retournées par les APIMemoryPool<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 queMemory<T>
et les instancesSpan<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’unSpan<T>
pour la mémoire tampon sous-jacente nécessite d’abord d’obtenir une instanceMemory<T>
, puis unSpan<T>
. Cela est assez coûteux, et souvent inutile, car leMemory<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 interneT[]
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’APIAllocate(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 queArrayPool<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 APISlice(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 deIMemoryOwner<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’unSpan<T>
sur lequel travailler, en raison de son caractère d’interface, et nous devons toujours obtenir une instance deMemory<T>
d’abord, puis unSpan<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.
.NET Community Toolkit