[DotNetBook] Étendue, mémoire et ReadOnlyMemory

Avec cet article, je continue de publier une série d'articles dont le résultat sera un livre sur les travaux du .NET CLR, et du .NET en général. Pour les liens - bienvenue au chat.


Mémoire <T> et ReadOnlyMemory <T>


Il existe deux différences visuelles entre la Memory<T> et la Span<T> . Le premier est que le type Memory<T> ne contient pas de contrainte ref dans l'en-tête de type. En d'autres termes, le type Memory<T> a le droit d'être non seulement sur la pile, étant soit une variable locale soit un paramètre de la méthode ou sa valeur de retour, mais également sur le tas, se référant à partir de là à certaines données en mémoire. Cependant, cette petite différence fait une énorme différence dans le comportement et les capacités de Memory<T> par rapport à Span<T> . Contrairement à Span<T> , qui est un moyen d'utiliser un certain tampon de données pour certaines méthodes, le type Memory<T> conçu pour stocker des informations sur le tampon et non pour fonctionner avec.


Remarque


Le chapitre publié sur Habré n'est pas mis à jour et, probablement, est un peu dépassé. Et par conséquent, veuillez vous tourner vers l'original pour un texte plus récent:



De là vient la différence dans l'API:


  • Memory<T> ne contient pas de méthodes d'accès aux données qu'elle gère. Au lieu de cela, il a la propriété Span et la méthode Slice , qui retournent le cheval de bataille - une instance du type Span .
  • Memory<T> contient en outre la méthode Pin() , conçue pour les scripts lorsque le tampon stocké doit être transmis à du code unsafe . Lorsqu'il est appelé pour les cas où la mémoire a été allouée dans .NET, le tampon est épinglé et ne bouge pas lorsque le GC est déclenché, renvoyant à l'utilisateur une instance de la structure MemoryHandle , qui encapsule le concept d'une GCHandle vie GCHandle qui a fixé le tampon en mémoire:

 public unsafe struct MemoryHandle : IDisposable { private void* _pointer; private GCHandle _handle; private IPinnable _pinnable; /// <summary> ///  MemoryHandle    /// </summary> public MemoryHandle(void* pointer, GCHandle handle = default, IPinnable pinnable = default) { _pointer = pointer; _handle = handle; _pinnable = pinnable; } /// <summary> ///     ,   ,       /// </summary> [CLSCompliant(false)] public void* Pointer => _pointer; /// <summary> ///  _handle  _pinnable,      /// </summary> public void Dispose() { if (_handle.IsAllocated) { _handle.Free(); } if (_pinnable != null) { _pinnable.Unpin(); _pinnable = null; } _pointer = null; } } 

Cependant, pour commencer, je propose de me familiariser avec l'ensemble des classes. Et en tant que premier d'entre eux, jetez un œil à la structure de la Memory<T> (tous les membres de type ne sont pas affichés, mais ceux qui semblent être les plus importants):


  public readonly struct Memory<T> { private readonly object _object; private readonly int _index, _length; public Memory(T[] array) { ... } public Memory(T[] array, int start, int length) { ... } internal Memory(MemoryManager<T> manager, int length) { ... } internal Memory(MemoryManager<T> manager, int start, int length) { ... } public int Length => _length & RemoveFlagsBitMask; public bool IsEmpty => (_length & RemoveFlagsBitMask) == 0; public Memory<T> Slice(int start, int length); public void CopyTo(Memory<T> destination) => Span.CopyTo(destination.Span); public bool TryCopyTo(Memory<T> destination) => Span.TryCopyTo(destination.Span); } 

En plus de spécifier les champs de structure, j'ai décidé de souligner en outre qu'il y a deux autres constructeurs de type internal travaillant sur la base d'une autre entité encore - le MemoryManager , qui sera discuté un peu plus loin et ce n'est pas quelque chose que vous pourriez simplement avoir pensée: un gestionnaire de mémoire au sens classique. Cependant, comme Span , Memory contient également une référence à l'objet qui sera parcouru, ainsi que le décalage et la taille du tampon interne. En outre, il convient de noter que la Memory ne peut être créée avec le new opérateur que sur la base du tableau et des méthodes d'extension - sur la base de la chaîne, du tableau et de ArraySegment . C'est-à-dire sa création sur la base d'une mémoire non gérée manuellement n'est pas implicite. Cependant, comme nous le voyons, il existe une méthode interne pour créer cette structure basée sur le MemoryManager :


File MemoryManager.cs


 public abstract class MemoryManager<T> : IMemoryOwner<T>, IPinnable { public abstract MemoryHandle Pin(int elementIndex = 0); public abstract void Unpin(); public virtual Memory<T> Memory => new Memory<T>(this, GetSpan().Length); public abstract Span<T> GetSpan(); protected Memory<T> CreateMemory(int length) => new Memory<T>(this, length); protected Memory<T> CreateMemory(int start, int length) => new Memory<T>(this, start, length); void IDisposable.Dispose() protected abstract void Dispose(bool disposing); } 

Je vais me permettre de discuter un peu de la terminologie introduite dans la commande CLR, en nommant le type par le nom MemoryManager. Quand je l'ai vu, j'ai d'abord décidé que ce serait quelque chose comme une gestion de la mémoire, mais manuelle, autre que LOH / SOH. Mais il était très déçu de voir la réalité. Vous devriez peut-être l'appeler par analogie avec l'interface: MemoryOwner.

Ce qui résume le concept du propriétaire d'un morceau de mémoire. En d'autres termes, si Span est un moyen de travailler avec la mémoire, la Memory est un moyen de stocker des informations sur un site particulier, alors MemoryManager est un moyen de contrôler sa vie, son propriétaire. Par exemple, vous pouvez prendre le type NativeMemoryManager<T> , qui, bien qu'écrit pour les tests, ne reflète pas mal l'essence du concept de "propriété":


Fichier NativeMemoryManager.cs


 internal sealed class NativeMemoryManager : MemoryManager<byte> { private readonly int _length; private IntPtr _ptr; private int _retainedCount; private bool _disposed; public NativeMemoryManager(int length) { _length = length; _ptr = Marshal.AllocHGlobal(length); } public override void Pin() { ... } public override void Unpin() { lock (this) { if (_retainedCount > 0) { _retainedCount--; if (_retainedCount == 0) { if (_disposed) { Marshal.FreeHGlobal(_ptr); _ptr = IntPtr.Zero; } } } } } //   } 

En d'autres termes, la classe offre la possibilité d'appels imbriqués à la méthode Pin() , comptant ainsi les liens résultants du monde unsafe .


Une autre entité étroitement liée à la Memory est MemoryPool , qui fournit la mise en commun des instances de MemoryManager (et en fait, IMemoryOwner ):


Fichier MemoryPool.cs


 public abstract class MemoryPool<T> : IDisposable { public static MemoryPool<T> Shared => s_shared; public abstract IMemoryOwner<T> Rent(int minBufferSize = -1); public void Dispose() { ... } } 

Qui est conçu pour émettre des tampons de la taille requise pour une utilisation temporaire. Les instances louées qui implémentent l' IMemoryOwner<T> ont une méthode Dispose() qui renvoie le tableau loué au pool de tableaux. Et par défaut, vous pouvez utiliser le pool de mémoire tampon partagé, qui est construit sur la base d' ArrayMemoryPool :


Fichier ArrayMemoryPool.cs


 internal sealed partial class ArrayMemoryPool<T> : MemoryPool<T> { private const int MaximumBufferSize = int.MaxValue; public sealed override int MaxBufferSize => MaximumBufferSize; public sealed override IMemoryOwner<T> Rent(int minimumBufferSize = -1) { if (minimumBufferSize == -1) minimumBufferSize = 1 + (4095 / Unsafe.SizeOf<T>()); else if (((uint)minimumBufferSize) > MaximumBufferSize) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.minimumBufferSize); return new ArrayMemoryPoolBuffer(minimumBufferSize); } protected sealed override void Dispose(bool disposing) { } } 

Et sur la base de ce qu'il a vu, l'image suivante du monde se profile:


  • Le type de données Span doit être utilisé dans les paramètres de la méthode si vous voulez lire des données ( ReadOnlySpan ) ou écrire ( Span ). Mais pas la tâche de le stocker dans le champ de classe pour une utilisation future
  • Si vous devez stocker un lien vers le tampon de données à partir du champ de classe, vous devez utiliser Memory<T> ou ReadOnlyMemory<T> - selon le but
  • MemoryManager<T> est le propriétaire du tampon de données (vous ne pouvez pas l'utiliser: si nécessaire). Il est nécessaire lorsque, par exemple, il est nécessaire de compter les appels vers Pin() . Ou quand vous avez besoin de savoir comment libérer de la mémoire
  • Si la Memory construite autour d'une zone de mémoire non gérée, Pin() fera rien. Cependant, cela unifie le travail avec différents types de tampons: à la fois dans le cas de code managé et dans le cas de code non managé, l'interface d'interaction sera la même
  • Chacun des types a des constructeurs publics. Cela signifie que vous pouvez utiliser les deux Span directement et en obtenir une copie de la Memory . Vous pouvez créer la Memory elle-même séparément ou organiser pour elle un type IMemoryOwner qui possédera la partie de la mémoire à laquelle la Memory se IMemoryOwner . Un cas particulier peut être n'importe quel type basé sur MemoryManager : une propriété locale d'un morceau de mémoire (par exemple, avec le comptage de références d'un monde unsafe ). Si en même temps vous devez extraire de tels tampons (attendez-vous à un trafic fréquent de tampons de taille approximativement égale), vous pouvez utiliser le type MemoryPool .
  • S'il est implicite que vous devez travailler avec du code unsafe , en y passant un certain tampon de données, vous devez utiliser le type de Memory : il a une méthode Pin qui automatise la fixation du tampon dans le tas .NET, s'il en a été créé un.
  • Si vous avez du trafic tampon (par exemple, vous résolvez le problème de l'analyse du texte du programme ou de certains DSL), il vaut la peine d'utiliser le type MemoryPool , qui peut être organisé de manière très correcte, en sortant des tampons de la taille appropriée à partir du pool (par exemple, un peu plus grand si pas approprié) mais avec l'élagage originalMemory.Slice(requiredSize) pour ne pas fragmenter le pool)

Lien vers le livre entier



Source: https://habr.com/ru/post/fr420051/


All Articles