
Exemples d'utilisation de Span <T>
Un être humain par nature ne peut pas pleinement comprendre le but d'un certain instrument tant qu'il n'a pas acquis une certaine expérience. Passons donc à quelques exemples.
ValueStringBuilder
L'un des exemples les plus intéressants en ce qui concerne les algorithmes est le type ValueStringBuilder
. Cependant, il est enfoui profondément dans mscorlib et marqué avec le modificateur internal
comme de nombreux autres types de données très intéressants. Cela signifie que nous ne trouverions pas cet instrument d'optimisation remarquable si nous n'avions pas recherché le code source de mscorlib.
Quel est le principal inconvénient du type de système StringBuilder
? Son principal inconvénient est le type et sa base - c'est un type de référence et est basé sur char[]
, c'est-à-dire un tableau de caractères. Au moins, cela signifie deux choses: nous utilisons le tas (mais pas beaucoup) de toute façon et augmentons les chances de rater l'argent du processeur.
Un autre problème avec StringBuilder
que j'ai rencontré est la construction de petites chaînes, c'est-à-dire lorsque la chaîne résultante doit être courte, par exemple moins de 100 caractères. Un formatage court pose des problèmes de performances.
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 .
$"{x} is in range [{min};{max}]"
Dans quelle mesure cette variante est-elle pire que la construction manuelle via StringBuilder
? La réponse n'est pas toujours évidente. Cela dépend du lieu de construction et de la fréquence d'appel de cette méthode. Initialement, string.Format
alloue de la mémoire pour StringBuilder
interne qui créera un tableau de caractères (SourceString.Length + args.Length * 8). Si lors de la construction du tableau, il s'avère que la longueur a été incorrectement déterminée, un autre StringBuilder
sera créé pour construire le reste. Cela conduira à la création d'une seule liste chaînée. Par conséquent, il doit retourner la chaîne construite, ce qui signifie une autre copie. C'est un gaspillage. Ce serait formidable si nous pouvions nous débarrasser de l'allocation du tableau d'une chaîne formée sur le tas: cela résoudrait l'un de nos problèmes.
Regardons ce type depuis la profondeur de mscorlib
:
Classe ValueStringBuilder
/ src / mscorlib / shared / System / Text / ValueStringBuilder
internal ref struct ValueStringBuilder { // this field will be active if we have too many characters private char[] _arrayToReturnToPool; // this field will be the main private Span<char> _chars; private int _pos; // the type accepts the buffer from the outside, delegating the choice of its size to a calling party public ValueStringBuilder(Span<char> initialBuffer) { _arrayToReturnToPool = null; _chars = initialBuffer; _pos = 0; } public int Length { get => _pos; set { int delta = value - _pos; if (delta > 0) { Append('\0', delta); } else { _pos = value; } } } // Here we get the string by copying characters from the array into another array public override string ToString() { var s = new string(_chars.Slice(0, _pos)); Clear(); return s; } // To insert a required character into the middle of the string //you should add space into the characters of that string and then copy that character public void Insert(int index, char value, int count) { if (_pos > _chars.Length - count) { Grow(count); } int remaining = _pos - index; _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); _chars.Slice(index, count).Fill(value); _pos += count; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(char c) { int pos = _pos; if (pos < _chars.Length) { _chars[pos] = c; _pos = pos + 1; } else { GrowAndAppend(c); } } [MethodImpl(MethodImplOptions.NoInlining)] private void GrowAndAppend(char c) { Grow(1); Append(c); } // If the original array passed by the constructor wasn't enough // we allocate an array of a necessary size from the pool of free arrays // It would be ideal if the algorithm considered // discreteness of array size to avoid pool fragmentation. [MethodImpl(MethodImplOptions.NoInlining)] private void Grow(int requiredAdditionalCapacity) { Debug.Assert(requiredAdditionalCapacity > _chars.Length - _pos); char[] poolArray = ArrayPool<char>.Shared.Rent(Math.Max(_pos + requiredAdditionalCapacity, _chars.Length * 2)); _chars.CopyTo(poolArray); char[] toReturn = _arrayToReturnToPool; _chars = _arrayToReturnToPool = poolArray; if (toReturn != null) { ArrayPool<char>.Shared.Return(toReturn); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Clear() { char[] toReturn = _arrayToReturnToPool; this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again if (toReturn != null) { ArrayPool<char>.Shared.Return(toReturn); } } // Missing methods: the situation is crystal clear private void AppendSlow(string s); public bool TryCopyTo(Span<char> destination, out int charsWritten); public void Append(string s); public void Append(char c, int count); public unsafe void Append(char* value, int length); public Span<char> AppendSpan(int length); }
Cette classe est fonctionnellement similaire à son collègue aîné StringBuilder
, bien qu'elle ait une caractéristique intéressante et très importante: c'est un type de valeur. Cela signifie qu'il est stocké et transmis entièrement par valeur. De plus, un nouveau modificateur de type ref
, qui fait partie d'une signature de déclaration de type, indique que ce type a une contrainte supplémentaire: il ne peut être alloué que sur la pile. Je veux dire que passer ses instances aux champs de classe produira une erreur. À quoi servent tous ces trucs? Pour répondre à cette question, il suffit de regarder la classe StringBuilder
, dont nous venons de décrire l'essence:
Classe StringBuilder /src/mscorlib/src/System/Text/StringBuilder.cs
public sealed class StringBuilder : ISerializable { // A StringBuilder is internally represented as a linked list of blocks each of which holds // a chunk of the string. It turns out string as a whole can also be represented as just a chunk, // so that is what we do. internal char[] m_ChunkChars; // The characters in this block internal StringBuilder m_ChunkPrevious; // Link to the block logically before this block internal int m_ChunkLength; // The index in m_ChunkChars that represent the end of the block internal int m_ChunkOffset; // The logical offset (sum of all characters in previous blocks) internal int m_MaxCapacity = 0; // ... internal const int DefaultCapacity = 16;
StringBuilder
est une classe qui contient une référence à un tableau de caractères. Ainsi, lorsque vous le créez, deux objets apparaissent en fait: StringBuilder
et un tableau de caractères d'au moins 16 caractères. C'est pourquoi il est essentiel de définir la longueur attendue d'une chaîne: elle sera construite en générant une seule liste chaînée de tableaux de 16 caractères chacun. Admettez, c'est un gaspillage. En termes de type ValueStringBuilder
, cela signifie aucune capacity
par défaut, car il emprunte de la mémoire externe. En outre, il s'agit d'un type de valeur et il oblige un utilisateur à allouer un tampon pour les caractères de la pile. Ainsi, toute l'instance d'un type est placée sur la pile avec son contenu et le problème d'optimisation est résolu. Comme il n'est pas nécessaire d'allouer de mémoire sur le tas, il n'y a aucun problème avec une diminution des performances lors du traitement du tas. Donc, vous pourriez avoir une question: pourquoi n'utilisons-nous pas toujours ValueStringBuilder
(ou son analogue personnalisé car nous ne pouvons pas utiliser l'original parce qu'il est interne)? La réponse est: cela dépend d'une tâche. Une chaîne résultante aura-t-elle une taille définie? Aura-t-il une longueur maximale connue? Si vous répondez «oui» et si la chaîne ne dépasse pas les limites raisonnables, vous pouvez utiliser la version de valeur de StringBuilder
. Cependant, si vous attendez de longues chaînes, utilisez la version habituelle.
ValueListBuilder
internal ref partial struct ValueListBuilder<T> { private Span<T> _span; private T[] _arrayFromPool; private int _pos; public ValueListBuilder(Span<T> initialSpan) { _span = initialSpan; _arrayFromPool = null; _pos = 0; } public int Length { get; set; } public ref T this[int index] { get; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(T item); public ReadOnlySpan<T> AsSpan(); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose(); private void Grow(); }
Le deuxième type de données que je veux particulièrement noter est le type ValueListBuilder
. Il est utilisé lorsque vous devez créer une collection d'éléments pendant une courte période et la transmettre à un algorithme pour le traitement.
Admettez, cette tâche ressemble assez à la tâche ValueStringBuilder
. Et cela se résout de la même manière:
Fichier ValueListBuilder.cs coreclr / src /../ Generic / ValueListBuilder.cs
Pour le dire clairement, ces situations sont souvent. Cependant, auparavant, nous avons résolu le problème d'une autre manière. Nous avions l'habitude de créer une List
, de la remplir de données et de perdre sa référence. Si la méthode est appelée fréquemment, cela conduira à une situation triste: de nombreuses instances de List
(et tableaux associés) sont suspendues sur le tas. Ce problème est maintenant résolu: aucun objet supplémentaire ne sera créé. Cependant, comme dans le cas de ValueStringBuilder
il n'est résolu que pour les programmeurs Microsoft: cette classe a le modificateur internal
.
Règles et pratiques d'utilisation
Pour bien comprendre le nouveau type de données, vous devez jouer avec en écrivant deux ou trois méthodes ou plus qui les utilisent. Cependant, il est possible d'apprendre les règles principales dès maintenant:
- Si votre méthode traite un ensemble de données en entrée sans modifier sa taille, vous pouvez essayer de vous en tenir au type
Span
. Si vous n'allez pas modifier le tampon, choisissez le type ReadOnlySpan
; - Si votre méthode gère les chaînes calculant des statistiques ou analysant ces chaînes, elle doit accepter
ReadOnlySpan<char>
. Le must est une nouvelle règle. Parce que lorsque vous acceptez une chaîne, vous demandez à quelqu'un de créer une sous-chaîne pour vous; - Si vous devez créer un court tableau de données (pas plus de 10 Ko) pour une méthode, vous pouvez facilement organiser cela en utilisant
Span<TType> buf = stackalloc TType[size]
. Notez que TType doit être un type de valeur car stackalloc
fonctionne stackalloc
avec les types de valeur.
Dans d'autres cas, vous feriez mieux de regarder de plus près la Memory
ou d'utiliser des types de données classiques.
Comment fonctionne span?
Je voudrais dire quelques mots supplémentaires sur le fonctionnement de Span
et pourquoi il est si remarquable. Et il y a de quoi parler. Ce type de données a deux versions: une pour .NET Core 2.0+ et l'autre pour le reste.
Fichier Span.Fast.cs, .NET Core 2.0 coreclr /.../ System / Span.Fast.cs **
public readonly ref partial struct Span<T> { /// A reference to a .NET object or a pure pointer internal readonly ByReference<T> _pointer; /// The length of the buffer based on the pointer private readonly int _length; // ... }
Fichier ??? [décompilé]
public ref readonly struct Span<T> { private readonly System.Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length; // ... }
Le truc, c'est que les énormes .NET Framework et .NET Core 1. * n'ont pas de garbage collector mis à jour de manière spéciale (contrairement à .NET Core 2.0+) et ils doivent utiliser un pointeur supplémentaire vers le début d'un tampon dans utiliser. Cela signifie que Span
gère en interne les objets .NET gérés comme s'ils n'étaient pas gérés. Regardez la deuxième variante de la structure: elle a trois champs. Le premier est une référence à un objet mangé. Le second est le décalage en octets depuis le début de cet objet, utilisé pour définir le début du tampon de données (dans les chaînes, ce tampon contient des caractères char
tandis que dans les tableaux, il contient les données d'un tableau). Enfin, le troisième champ contient la quantité d'éléments dans le tampon posé sur une ligne.
Voyons comment Span
gère les chaînes, par exemple:
Fichier MemoryExtensions.Fast.cs
coreclr /../ MemoryExtensions.Fast.cs
public static ReadOnlySpan<char> AsSpan(this string text) { if (text == null) return default; return new ReadOnlySpan<char>(ref text.GetRawStringData(), text.Length); }
Où string.GetRawStringData()
ressemble à ceci:
Un fichier avec la définition des champs coreclr /../ System / String.CoreCLR.cs
Un fichier avec la définition de GetRawStringData coreclr /../ System / String.cs
public sealed partial class String : IComparable, IEnumerable, IConvertible, IEnumerable<char>, IComparable<string>, IEquatable<string>, ICloneable { // // These fields map directly onto the fields in an EE StringObject. See object.h for the layout. // [NonSerialized] private int _stringLength; // For empty strings, this will be '\0' since // strings are both null-terminated and length prefixed [NonSerialized] private char _firstChar; internal ref char GetRawStringData() => ref _firstChar; }
Il s'avère que la méthode accède directement à l'intérieur de la chaîne, tandis que la spécification ref char
permet à GC de suivre une référence non gérée à l'intérieur de la chaîne en la déplaçant avec la chaîne lorsque GC est actif.
La même chose s'applique aux tableaux: lorsque Span
est créé, un code JIT interne calcule le décalage pour le début du tableau de données et initialise Span
avec ce décalage. La façon dont vous pouvez calculer le décalage pour les chaînes et les tableaux a été discutée dans le chapitre sur la structure des objets en mémoire (. \ ObjectsStructure.md).
Span <T> comme valeur retournée
Malgré toute l'harmonie, Span
a des contraintes logiques mais inattendues sur son retour d'une méthode. Si nous regardons le code suivant:
unsafe void Main() { var x = GetSpan(); } public Span<byte> GetSpan() { Span<byte> reff = new byte[100]; return reff; }
nous pouvons voir que c'est logique et bon. Cependant, si nous remplaçons une instruction par une autre:
unsafe void Main() { var x = GetSpan(); } public Span<byte> GetSpan() { Span<byte> reff = stackalloc byte[100]; return reff; }
un compilateur l'interdira. Avant de dire pourquoi, j'aimerais que vous deviniez quels problèmes cette construction pose.
Eh bien, j'espère que vous avez pensé, deviné et peut-être même compris la raison. Si oui, mes efforts pour écrire un chapitre détaillé sur une [pile de threads] (./ThreadStack.md) ont porté leurs fruits. Parce que lorsque vous renvoyez une référence à des variables locales à partir d'une méthode qui termine son travail, vous pouvez appeler une autre méthode, attendre qu'elle termine également son travail, puis lire les valeurs de ces variables locales à l'aide de x [0.99].
Heureusement, lorsque nous tentons d'écrire un tel code, un compilateur tape sur nos poignets en avertissant: CS8352 Cannot use local 'reff' in this context because it may expose referenced variables outside of their declaration scope
. Le compilateur a raison car si vous contournez cette erreur, il y aura une chance, pendant que vous êtes dans un plug-in, de voler les mots de passe des autres ou d'élever les privilèges pour exécuter notre plug-in.
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 .