
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 modificateur ref
dans l'en-tête du type. En d'autres termes, le type Memory<T>
peut être alloué à la fois sur la pile tout en étant soit une variable locale, soit un paramètre de méthode, soit sa valeur retournée et sur le tas, référençant certaines données en mémoire à partir de là. Cependant, cette petite différence crée une énorme distinction dans le comportement et les capacités de Memory<T>
par rapport à Span<T>
. Contrairement à Span<T>
qui est un instrument permettant à certaines méthodes d'utiliser un tampon de données, le type Memory<T>
est conçu pour stocker des informations sur le tampon, mais pas pour le gérer. Ainsi, il y a la différence dans l'API.
Memory<T>
ne dispose pas de méthodes pour accéder aux données dont elle est responsable. Au lieu de cela, il a la propriété Span
et la méthode Slice
qui retournent une instance du type Span
.- En outre,
Memory<T>
contient la méthode Pin()
utilisée pour les scénarios où des données de mémoire tampon stockées doivent être transmises à un code unsafe
. Si cette méthode est appelée lorsque la mémoire est allouée dans .NET, le tampon sera épinglé et ne se déplacera pas lorsque GC est actif. Cette méthode renvoie une instance de la structure MemoryHandle
, qui encapsule GCHandle
pour indiquer un segment d'une durée de vie et pour épingler le tampon de tableau en mémoire.
Ce chapitre a été traduit du russe conjointement par l'auteur et par des traducteurs professionnels . Vous pouvez nous aider avec la traduction du russe ou de l'anglais dans n'importe quelle autre langue, principalement en chinois ou en allemand.
Aussi, si vous voulez nous remercier, la meilleure façon de le faire est de nous donner une étoile sur github ou sur fork repository
github / sidristij / dotnetbook .
Cependant, je suggère que nous nous familiarisions avec l'ensemble des classes. Tout d'abord, regardons la structure Memory<T>
elle-même (ici, je montre uniquement les membres de type que j'ai trouvé 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); public Span<T> Span { get; } public unsafe MemoryHandle Pin(); }
Comme nous le voyons, la structure contient le constructeur basé sur des tableaux, mais stocke des données dans l'objet. Il s'agit de référencer en outre des chaînes qui n'ont pas de constructeur conçu pour elles, mais qui peuvent être utilisées avec la méthode de string
AsMemory()
, elle renvoie ReadOnlyMemory
. Cependant, comme les deux types doivent être binaires similaires, Object
est le type du champ _object
.
Ensuite, nous voyons deux constructeurs basés sur MemoryManager
. Nous en parlerons plus tard. Les propriétés d'obtention de Length
(taille) et IsEmpty
recherchent un ensemble vide. Il existe également la méthode Slice
pour obtenir un sous-ensemble ainsi que les méthodes de copie CopyTo
et TryCopyTo
.
En parlant de Memory
je veux décrire en détail deux méthodes de ce type: la propriété Span
et la méthode Pin
.
Mémoire <T> .Span
public Span<T> Span { get { if (_index < 0) { return ((MemoryManager<T>)_object).GetSpan().Slice(_index & RemoveFlagsBitMask, _length); } else if (typeof(T) == typeof(char) && _object is string s) { // This is dangerous, returning a writable span for a string that should be immutable. // However, we need to handle the case where a ReadOnlyMemory<char> was created from a string // and then cast to a Memory<T>. Such a cast can only be done with unsafe or marshaling code, // in which case that's the dangerous operation performed by the dev, and we're just following // suit here to make it work as best as possible. return new Span<T>(ref Unsafe.As<char, T>(ref s.GetRawStringData()), s.Length).Slice(_index, _length); } else if (_object != null) { return new Span<T>((T[])_object, _index, _length & RemoveFlagsBitMask); } else { return default; } } }
À savoir, les lignes qui gèrent la gestion des chaînes. Ils disent que si nous convertissons ReadOnlyMemory<T>
en Memory<T>
(ces choses sont les mêmes dans la représentation binaire et il y a même un commentaire ces types doivent coïncider de manière binaire car l'un est produit à partir d'un autre en appelant Unsafe.As
) nous aurons un ~ accès à une chambre secrète ~ avec la possibilité de changer de chaîne. Il s'agit d'un mécanisme extrêmement dangereux:
unsafe void Main() { var str = "Hello!"; ReadOnlyMemory<char> ronly = str.AsMemory(); Memory<char> mem = (Memory<char>)Unsafe.As<ReadOnlyMemory<char>, Memory<char>>(ref ronly); mem.Span[5] = '?'; Console.WriteLine(str); } --- Hello?
Ce mécanisme combiné à l'internement de chaînes peut avoir des conséquences désastreuses.
Mémoire <T> .Pin
La deuxième méthode qui attire l'attention est Pin
:
public unsafe MemoryHandle Pin() { if (_index < 0) { return ((MemoryManager<T>)_object).Pin((_index & RemoveFlagsBitMask)); } else if (typeof(T) == typeof(char) && _object is string s) { // This case can only happen if a ReadOnlyMemory<char> was created around a string // and then that was cast to a Memory<char> using unsafe / marshaling code. This needs // to work, however, so that code that uses a single Memory<char> field to store either // a readable ReadOnlyMemory<char> or a writable Memory<char> can still be pinned and // used for interop purposes. GCHandle handle = GCHandle.Alloc(s, GCHandleType.Pinned); void* pointer = Unsafe.Add<T>(Unsafe.AsPointer(ref s.GetRawStringData()), _index); return new MemoryHandle(pointer, handle); } else if (_object is T[] array) { // Array is already pre-pinned if (_length < 0) { void* pointer = Unsafe.Add<T>(Unsafe.AsPointer(ref array.GetRawSzArrayData()), _index); return new MemoryHandle(pointer); } else { GCHandle handle = GCHandle.Alloc(array, GCHandleType.Pinned); void* pointer = Unsafe.Add<T>(Unsafe.AsPointer(ref array.GetRawSzArrayData()), _index); return new MemoryHandle(pointer, handle); } } return default; }
C'est également un instrument important pour l'unification car si nous voulons passer un tampon à du code non managé, il suffit d'appeler la méthode Pin()
et de passer un pointeur vers ce code quel que soit le type de données Memory<T>
la Memory<T>
fait référence. Ce pointeur sera stocké dans la propriété d'une structure résultante.
void PinSample(Memory<byte> memory) { using(var handle = memory.Pin()) { WinApi.SomeApiMethod(handle.Pointer); } }
Peu importe ce que Pin()
été appelé dans ce code: il peut s'agir de Memory
qui représente soit T[]
, soit une string
ou un tampon de mémoire non managée. Seuls les tableaux et les chaînes obtiendront un vrai GCHandle.Alloc(array, GCHandleType.Pinned)
et en cas de mémoire non gérée, rien ne se passera.
MemoryManager, IMemoryOwner, MemoryPool
En plus d'indiquer les champs de structure, je tiens à noter qu'il existe deux autres constructeurs de type internal
basés sur une autre entité - MemoryManager
. Ce n'est pas un gestionnaire de mémoire classique auquel vous auriez pu penser et nous allons en parler plus tard. gestionnaire de mémoire classique auquel vous avez peut-être pensé et nous allons en parler plus tard. Comme Span
, la Memory
a une référence à un objet navigué, un décalage et une taille de tampon interne. Notez que vous pouvez utiliser le new
opérateur pour créer de la Memory
partir d'un tableau uniquement. Ou, vous pouvez utiliser des méthodes d'extension pour créer de la Memory
partir d'une chaîne, d'un tableau ou d'un ArraySegment
. Je veux dire qu'il n'est pas conçu pour être créé manuellement à partir de la mémoire non gérée. Cependant, nous pouvons voir qu'il existe une méthode interne pour créer cette structure à l'aide de 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); }
Cette structure indique le propriétaire d'une plage de mémoire. En d'autres termes, Span
est un instrument pour travailler avec la mémoire, Memory
est un outil pour stocker les informations sur une plage de mémoire particulière et MemoryManager
est un outil pour contrôler la durée de vie de cette plage, c'est-à-dire son propriétaire. Par exemple, nous pouvons regarder le type NativeMemoryManager<T>
. Bien qu'il soit utilisé pour des tests, ce type représente clairement le 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; } } } } } // Other methods }
Cela signifie que la classe autorise les appels imbriqués de la méthode Pin()
, comptant ainsi les références générées à partir du monde unsafe
.
Une autre entité étroitement liée à la Memory
est MemoryPool
qui MemoryManager
instances de IMemoryOwner
( IMemoryOwner
en fait):
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() { ... } }
Il est utilisé pour louer des tampons d'une taille nécessaire pour une utilisation temporaire. Les instances louées avec l' IMemoryOwner<T>
implémentée ont la méthode Dispose()
pour renvoyer le tableau loué au pool de tableaux. Par défaut, vous pouvez utiliser le pool de tampons partageable construit sur 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) { } }
Sur la base de cette architecture, nous avons l'image suivante:
Span
type de données Span
doit être utilisé comme paramètre de méthode si vous souhaitez lire des données ( ReadOnlySpan
) ou lire et écrire des données ( Span
). Cependant, il n'est pas censé être stocké dans un champ d'une classe pour une utilisation future.- Si vous devez stocker une référence d'un champ d'une classe dans un tampon de données, vous devez utiliser
Memory<T>
ou ReadOnlyMemory<T>
selon vos objectifs. MemoryManager<T>
est propriétaire d'un tampon de données (facultatif). Cela peut être nécessaire si vous devez compter les appels Pin()
par exemple. Ou, si vous avez besoin de savoir comment libérer de la mémoire.- Si la
Memory
est construite autour d'une plage de mémoire non gérée, Pin()
ne peut rien faire. Cependant, cela uniformise le travail avec différents types de tampons: pour le code managé et non managé, l'interface d'interaction sera la même. - Chaque type a des constructeurs publics. Cela signifie que vous pouvez utiliser
Span
directement ou obtenir son instance de la Memory
. Pour la Memory
tant que telle, vous pouvez la créer individuellement ou vous pouvez créer une plage de mémoire appartenant à IMemoryOwner
et référencée par la Memory
. Tout type basé sur MemoryManger
peut être considéré comme un cas spécifique auquel il possède une plage de mémoire locale (par exemple accompagné du comptage des références du monde unsafe
). De plus, si vous devez regrouper de tels tampons (le trafic fréquent de tampons de taille presque égale est attendu), vous pouvez utiliser le type MemoryPool
. - Si vous avez l'intention de travailler avec du code
unsafe
en y passant un tampon de données, vous devez utiliser le type de Memory
qui a la méthode Pin()
qui épingle automatiquement un tampon sur le tas .NET s'il a été créé à cet endroit. - Si vous avez du trafic de tampons (par exemple, vous analysez un texte d'un programme ou DSL), il est préférable d'utiliser le type
MemoryPool
. Vous pouvez l'implémenter correctement pour sortir les tampons d'une taille nécessaire à partir d'un pool (par exemple un tampon légèrement plus grand s'il n'y en a pas de convenable, mais en utilisant originalMemory.Slice(requiredSize)
pour éviter la fragmentation du pool).
Pour mesurer les performances de nouveaux types de données, j'ai décidé d'utiliser une bibliothèque qui est déjà devenue BenchmarkDotNet standard:
[Config(typeof(MultipleRuntimesConfig))] public class SpanIndexer { private const int Count = 100; private char[] arrayField; private ArraySegment<char> segment; private string str; [GlobalSetup] public void Setup() { str = new string(Enumerable.Repeat('a', Count).ToArray()); arrayField = str.ToArray(); segment = new ArraySegment<char>(arrayField); } [Benchmark(Baseline = true, OperationsPerInvoke = Count)] public int ArrayIndexer_Get() { var tmp = 0; for (int index = 0, len = arrayField.Length; index < len; index++) { tmp = arrayField[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public void ArrayIndexer_Set() { for (int index = 0, len = arrayField.Length; index < len; index++) { arrayField[index] = '0'; } } [Benchmark(OperationsPerInvoke = Count)] public int ArraySegmentIndexer_Get() { var tmp = 0; var accessor = (IList<char>)segment; for (int index = 0, len = accessor.Count; index < len; index++) { tmp = accessor[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public void ArraySegmentIndexer_Set() { var accessor = (IList<char>)segment; for (int index = 0, len = accessor.Count; index < len; index++) { accessor[index] = '0'; } } [Benchmark(OperationsPerInvoke = Count)] public int StringIndexer_Get() { var tmp = 0; for (int index = 0, len = str.Length; index < len; index++) { tmp = str[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public int SpanArrayIndexer_Get() { var span = arrayField.AsSpan(); var tmp = 0; for (int index = 0, len = span.Length; index < len; index++) { tmp = span[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public int SpanArraySegmentIndexer_Get() { var span = segment.AsSpan(); var tmp = 0; for (int index = 0, len = span.Length; index < len; index++) { tmp = span[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public int SpanStringIndexer_Get() { var span = str.AsSpan(); var tmp = 0; for (int index = 0, len = span.Length; index < len; index++) { tmp = span[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public void SpanArrayIndexer_Set() { var span = arrayField.AsSpan(); for (int index = 0, len = span.Length; index < len; index++) { span[index] = '0'; } } [Benchmark(OperationsPerInvoke = Count)] public void SpanArraySegmentIndexer_Set() { var span = segment.AsSpan(); for (int index = 0, len = span.Length; index < len; index++) { span[index] = '0'; } } } public class MultipleRuntimesConfig : ManualConfig { public MultipleRuntimesConfig() { Add(Job.Default .With(CsProjClassicNetToolchain.Net471) // Span not supported by CLR .WithId(".NET 4.7.1")); Add(Job.Default .With(CsProjCoreToolchain.NetCoreApp20) // Span supported by CLR .WithId(".NET Core 2.0")); Add(Job.Default .With(CsProjCoreToolchain.NetCoreApp21) // Span supported by CLR .WithId(".NET Core 2.1")); Add(Job.Default .With(CsProjCoreToolchain.NetCoreApp22) // Span supported by CLR .WithId(".NET Core 2.2")); } }
Voyons maintenant les résultats.

En les regardant, nous pouvons obtenir les informations suivantes:
ArraySegment
est horrible. Mais si vous l'enveloppez dans Span
vous pouvez le rendre moins horrible. Dans ce cas, les performances augmenteront 7 fois.- Si nous considérons .NET Framework 4.7.1 (la même chose est pour 4.5), l'utilisation de
Span
réduira considérablement les performances lorsque vous travaillez avec des tampons de données. Il diminuera de 30 à 35%. - Cependant, si nous regardons .NET Core 2.1+, les performances restent similaires ou même augmentent étant donné que
Span
peut utiliser une partie d'un tampon de données, créant le contexte. La même fonctionnalité peut être trouvée dans ArraySegment
, mais elle fonctionne très lentement.
Ainsi, nous pouvons tirer des conclusions simples concernant l'utilisation de ces types de données:
- pour
.NET Framework 4.5+
et .NET Core
ils ont le seul avantage: ils sont plus rapides que ArraySegment
lorsqu'ils traitent avec un sous-ensemble d'un tableau d'origine; - dans
.NET Core 2.1+
leur utilisation offre un avantage indéniable par rapport à ArraySegment
et à toute implémentation manuelle de Slice
; - les trois méthodes sont aussi productives que possible et cela ne peut être réalisé avec aucun outil pour unifier les baies.
Ce chapitre a été traduit du russe conjointement par l'auteur et par des traducteurs professionnels . Vous pouvez nous aider avec la traduction du russe ou de l'anglais dans n'importe quelle autre langue, principalement en chinois ou en allemand.
