Memoria y Span pt.2


Ejemplos de uso de Span <T>


Un ser humano por naturaleza no puede entender completamente el propósito de un determinado instrumento hasta que él o ella adquieran algo de experiencia. Entonces, pasemos a algunos ejemplos.


ValueStringBuilder


Uno de los ejemplos más interesantes con respecto a los algoritmos es el tipo ValueStringBuilder . Sin embargo, está enterrado en el interior de mscorlib y marcado con el modificador internal como muchos otros tipos de datos muy interesantes. Esto significa que no encontraríamos este notable instrumento para la optimización si no hemos investigado el código fuente de mscorlib.


¿Cuál es la principal desventaja del tipo de sistema StringBuilder ? Su principal inconveniente es el tipo y su base: es un tipo de referencia y se basa en char[] , es decir, una matriz de caracteres. Al menos, esto significa dos cosas: usamos el montón (aunque no mucho) de todos modos y aumentamos las posibilidades de perder el efectivo de la CPU.


Otro problema con StringBuilder que enfrenté es la construcción de pequeñas cadenas, es decir, cuando la cadena resultante debe ser corta, por ejemplo, menos de 100 caracteres. El formato corto plantea problemas de rendimiento.


Este capítulo fue traducido del ruso conjuntamente por el autor y por traductores profesionales . Puede ayudarnos con la traducción del ruso o el inglés a cualquier otro idioma, principalmente al chino o al alemán.

Además, si quieres agradecernos, la mejor manera de hacerlo es darnos una estrella en Github o bifurcar el repositorio github / sidristij / dotnetbook .

  $"{x} is in range [{min};{max}]" 

¿En qué medida es esta variante peor que la construcción manual a través de StringBuilder ? La respuesta no siempre es obvia. Depende del lugar de construcción y la frecuencia de llamar a este método. Inicialmente, string.Format asigna memoria para StringBuilder interno que creará una matriz de caracteres (SourceString.Length + args.Length * 8). Si durante la construcción de la matriz resulta que la longitud se determinó incorrectamente, se creará otro StringBuilder para construir el resto. Esto conducirá a la creación de una sola lista vinculada. Como resultado, debe devolver la cadena construida, lo que significa otra copia. Eso es un desperdicio. Sería genial si pudiéramos deshacernos de asignar la matriz de una cadena formada en el montón: esto resolvería uno de nuestros problemas.


Veamos este tipo desde la profundidad de mscorlib :


Clase 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); } 

Esta clase es funcionalmente similar a su compañero principal StringBuilder , aunque tiene una característica interesante y muy importante: es un tipo de valor. Eso significa que se almacena y se pasa completamente por valor. Además, un nuevo modificador de tipo de ref , que forma parte de una firma de declaración de tipo, indica que este tipo tiene una restricción adicional: solo se puede asignar en la pila. Quiero decir que pasar sus instancias a campos de clase producirá un error. ¿Para qué es todo esto? Para responder a esta pregunta, solo necesita mirar la clase StringBuilder , cuya esencia acabamos de describir:


Clase 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 es una clase que contiene una referencia a una matriz de caracteres. Por lo tanto, cuando lo crea, aparecen dos objetos de hecho: StringBuilder y una matriz de caracteres que tiene al menos 16 caracteres de tamaño. Esta es la razón por la cual es esencial establecer la longitud esperada de una cadena: se generará generando una única lista vinculada de matrices con 16 caracteres cada una. Admitir, eso es un desperdicio. En términos de tipo ValueStringBuilder , significa que no hay capacity predeterminada, ya que toma prestada memoria externa. Además, es un tipo de valor y hace que un usuario asigne un búfer para los caracteres en la pila. Por lo tanto, toda la instancia de un tipo se coloca en la pila junto con su contenido y se resuelve el problema de la optimización. Como no hay necesidad de asignar memoria en el montón, no hay problemas con una disminución en el rendimiento cuando se trata con el montón. Entonces, puede que tenga una pregunta: ¿por qué no siempre usamos ValueStringBuilder (o su análogo personalizado, ya que no podemos usar el original porque es interno)? La respuesta es: depende de una tarea. ¿Tendrá una cadena resultante un tamaño definido? ¿Tendrá una longitud máxima conocida? Si responde "sí" y si la cadena no supera los límites razonables, puede usar la versión de valor de StringBuilder . Sin embargo, si espera cadenas largas, use la versión habitual.


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(); } 

El segundo tipo de datos que quiero destacar especialmente es el tipo ValueListBuilder . Se usa cuando necesita crear una colección de elementos por un corto tiempo y pasarla a un algoritmo para su procesamiento.


Admita, esta tarea se parece bastante a la tarea ValueStringBuilder . Y se resuelve de manera similar:


Archivo ValueListBuilder.cs coreclr / src /../ Generic / ValueListBuilder.cs


Para decirlo claramente, estas situaciones son a menudo. Sin embargo, anteriormente resolvimos el problema de otra manera. Solíamos crear una List , llenarla con datos y perder la referencia a ella. Si se llama al método con frecuencia, esto conducirá a una situación triste: muchas instancias de la List (y las matrices asociadas) se suspenden en el montón. Ahora este problema está resuelto: no se crearán objetos adicionales. Sin embargo, como en el caso de ValueStringBuilder , se resuelve solo para programadores de Microsoft: esta clase tiene el modificador internal .


Reglas y práctica de uso


Para comprender completamente el nuevo tipo de datos, debe jugar con él escribiendo dos o tres o más métodos que lo usen. Sin embargo, es posible aprender las reglas principales en este momento:


  • Si su método procesa algún conjunto de datos de entrada sin cambiar su tamaño, puede intentar mantener el tipo Span . Si no va a modificar el búfer, elija el tipo ReadOnlySpan ;
  • Si su método maneja cadenas que calculan algunas estadísticas o analiza estas cadenas, debe aceptar ReadOnlySpan<char> . Debe es una nueva regla. Porque cuando aceptas una cadena, haces que alguien cree una subcadena para ti;
  • Si necesita crear una matriz de datos corta (no más de 10 kB) para un método, puede organizarlo fácilmente con Span<TType> buf = stackalloc TType[size] . Tenga en cuenta que TType debe ser un tipo de valor ya que stackalloc funciona solo con tipos de valor.

En otros casos, será mejor que mire más de cerca la Memory o use tipos de datos clásicos.


¿Cómo funciona span?


Me gustaría decir algunas palabras adicionales sobre cómo funciona Span y por qué es tan notable. Y hay algo de qué hablar. Este tipo de datos tiene dos versiones: una para .NET Core 2.0+ y la otra para el resto.


Archivo 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; // ... } 

Archivo ??? [descompilado]


 public ref readonly struct Span<T> { private readonly System.Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length; // ... } 

La cuestión es que .NET Framework y .NET Core 1. * no tienen un recolector de basura actualizado de una manera especial (a diferencia de .NET Core 2.0+) y tienen que usar un puntero adicional al comienzo de un búfer en uso. Eso significa que, internamente, Span maneja los objetos .NET administrados como si no estuvieran administrados. Solo mire la segunda variante de la estructura: tiene tres campos. El primero es una referencia a un objeto gestionado. El segundo es el desplazamiento en bytes desde el comienzo de este objeto, usado para definir el comienzo del búfer de datos (en cadenas este búfer contiene caracteres char mientras que en las matrices contiene los datos de una matriz). Finalmente, el tercer campo contiene la cantidad de elementos en el búfer colocado en una fila.


Veamos cómo Span maneja cadenas, por ejemplo:


File 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); } 

Donde string.GetRawStringData() ve de la siguiente manera:


Un archivo con la definición de campos coreclr /../ System / String.CoreCLR.cs


Un archivo con la definición 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; } 

Resulta que el método accede directamente al interior de la cadena, mientras que la especificación de ref char permite al GC rastrear una referencia no administrada al interior de la cadena moviéndola junto con la cadena cuando el GC está activo.


Lo mismo ocurre con las matrices: cuando se crea Span , algún código JIT interno calcula el desplazamiento para el comienzo de la matriz de datos e inicializa Span con este desplazamiento. La forma en que puede calcular el desplazamiento para cadenas y matrices se discutió en el capítulo sobre la estructura de los objetos en la memoria (. \ ObjectsStructure.md).


Span <T> como valor devuelto


A pesar de toda la armonía, Span tiene algunas limitaciones lógicas pero inesperadas en su regreso de un método. Si nos fijamos en el siguiente código:


 unsafe void Main() { var x = GetSpan(); } public Span<byte> GetSpan() { Span<byte> reff = new byte[100]; return reff; } 

podemos ver que es lógico y bueno. Sin embargo, si reemplazamos una instrucción con otra:


 unsafe void Main() { var x = GetSpan(); } public Span<byte> GetSpan() { Span<byte> reff = stackalloc byte[100]; return reff; } 

un compilador lo prohibirá. Antes de decir por qué, me gustaría que adivines qué problemas trae esta construcción.


Bueno, espero que hayas pensado, adivinado y tal vez incluso entendido la razón. En caso afirmativo, mis esfuerzos por escribir un capítulo detallado sobre una [pila de hilos] (./ThreadStack.md) valieron la pena. Porque cuando devuelve una referencia a variables locales de un método que finaliza su trabajo, puede llamar a otro método, esperar hasta que también termine su trabajo y luego leer los valores de esas variables locales usando x [0.99].


Afortunadamente, cuando intentamos escribir dicho código, un compilador golpea nuestras muñecas advirtiendo: CS8352 Cannot use local 'reff' in this context because it may expose referenced variables outside of their declaration scope . El compilador tiene razón porque si omite este error, habrá una posibilidad, mientras está en un complemento, de robar las contraseñas de otros o elevar los privilegios para ejecutar nuestro complemento.


Este capítulo fue traducido del ruso conjuntamente por el autor y por traductores profesionales . Puede ayudarnos con la traducción del ruso o el inglés a cualquier otro idioma, principalmente al chino o al alemán.

Además, si quieres agradecernos, la mejor manera de hacerlo es darnos una estrella en Github o bifurcar el repositorio github / sidristij / dotnetbook .

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


All Articles