Memoria y Span pt. 1

A partir de .NET Core 2.0 y .NET Framework 4.5 podemos usar nuevos tipos de datos: Span y Memory . Para usarlos, solo necesita instalar el paquete System.Memory nuget:


PM> Install-Package System.Memory

Estos tipos de datos son notables porque el equipo de CLR ha hecho un gran trabajo para implementar su soporte especial dentro del código del compilador JIT .NET Core 2.1+ incorporando estos tipos de datos directamente en el núcleo. ¿Qué tipo de datos son estos y por qué valen un capítulo entero?


Si hablamos de problemas que hicieron aparecer estos tipos, debería nombrar tres de ellos. El primero es el código no administrado.


Tanto el lenguaje como la plataforma han existido durante muchos años junto con los medios para trabajar con código no administrado. Entonces, ¿por qué lanzar otra API para trabajar con código no administrado si la primera existió básicamente durante muchos años? Para responder a esta pregunta, debemos entender lo que nos faltaba antes.


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 .

Los desarrolladores de la plataforma ya intentaron facilitarnos el uso de recursos no administrados. Implementaron envoltorios automáticos para métodos importados y cálculo de referencias que funciona automáticamente en la mayoría de los casos. Aquí también pertenece a stackalloc , mencionado en el capítulo sobre una pila de hilos. Sin embargo, como lo veo, los primeros desarrolladores de C # vinieron del mundo C ++ (mi caso), pero ahora cambian de lenguajes de más alto nivel (conozco un desarrollador que escribió antes en JavaScript). Esto significa que las personas se vuelven más sospechosas al código no administrado y las construcciones C / C +, tanto más a Assembler.


Como resultado, los proyectos contienen cada vez menos código inseguro y la confianza en la API de la plataforma crece cada vez más. Esto es fácil de verificar si buscamos casos de uso de stackalloc en repositorios públicos; son escasos. Sin embargo, tomemos cualquier código que lo use:


Clase Interop.ReadDir
/src/mscorlib/shared/Interop/Unix/System.Native/Interop.ReadDir.cs


 unsafe { // s_readBufferSize is zero when the native implementation does not support reading into a buffer. byte* buffer = stackalloc byte[s_readBufferSize]; InternalDirectoryEntry temp; int ret = ReadDirR(dir.DangerousGetHandle(), buffer, s_readBufferSize, out temp); // We copy data into DirectoryEntry to ensure there are no dangling references. outputEntry = ret == 0 ? new DirectoryEntry() { InodeName = GetDirectoryEntryName(temp), InodeType = temp.InodeType } : default(DirectoryEntry); return ret; } 

Podemos ver por qué no es popular. Simplemente lea este código y pregúntese si confía en él. Supongo que la respuesta es 'No'. Entonces, pregúntate por qué. Es obvio: no solo vemos la palabra Dangerous , que sugiere que algo puede salir mal, sino que existe la palabra clave unsafe y el byte* buffer = stackalloc byte[s_readBufferSize]; línea (específicamente - byte* ) que cambia nuestra actitud. Este es un detonante para que pienses: "¿No había otra forma de hacerlo"? Entonces, profundicemos en el psicoanálisis: ¿por qué podría pensar de esa manera? Por un lado, usamos construcciones de lenguaje y la sintaxis ofrecida aquí está lejos de, por ejemplo, C ++ / CLI, que permite cualquier cosa (incluso insertar código de ensamblador puro). Por otro lado, esta sintaxis parece inusual.


El segundo problema que los desarrolladores pensaron implícita o explícitamente es la incompatibilidad de los tipos string y char []. Aunque, lógicamente, una cadena es una matriz de caracteres, pero no puede convertir una cadena a char []: solo puede crear un nuevo objeto y copiar el contenido de una cadena a una matriz. Esta incompatibilidad se introduce para optimizar las cadenas en términos de almacenamiento (no hay matrices de solo lectura). Sin embargo, los problemas aparecen cuando comienza a trabajar con archivos. ¿Cómo leerlos? Como una cadena o como una matriz? Si elige una matriz, no puede usar algunos métodos diseñados para trabajar con cadenas. ¿Qué hay de leer como una cuerda? Puede ser muy largo Si necesita analizarlo entonces, qué analizador debería elegir para los tipos de datos primitivos: no siempre desea analizarlos manualmente (enteros, flotantes, dados en diferentes formatos). Tenemos muchos algoritmos probados que lo hacen de manera más rápida y eficiente, ¿no? Sin embargo, tales algoritmos a menudo funcionan con cadenas que no contienen nada más que un tipo primitivo. Entonces, hay un dilema.


El tercer problema es que los datos requeridos por un algoritmo rara vez forman un segmento de datos continuo y sólido dentro de una sección de una matriz leída de alguna fuente. Por ejemplo, en el caso de archivos o datos leídos de un socket, tenemos una parte de los que ya fueron procesados ​​por un algoritmo, seguidos de una parte de los datos que deben ser procesados ​​por nuestro método, y luego por datos aún no procesados. Idealmente, nuestro método solo desea los datos para los que se diseñó este método. Por ejemplo, un método que analiza enteros no estará contento con una cadena que contenga algunas palabras con un número esperado en algún lugar entre ellas. Este método quiere un número y nada más. O, si pasamos una matriz completa, hay un requisito para indicar, por ejemplo, el desplazamiento de un número desde el comienzo de la matriz.


 int ParseInt(char[] input, int index) { while(char.IsDigit(input[index])) { // ... index++; } } 

Sin embargo, este enfoque es pobre, ya que este método obtiene datos innecesarios. En otras palabras, el método se llama para contextos para los que no fue diseñado y tiene que resolver algunas tareas externas. Este es un mal diseño. ¿Cómo evitar estos problemas? Como opción, podemos usar el tipo ArraySegment<T> que puede dar acceso a una sección de una matriz:


 int ParseInt(IList<char>[] input) { while(char.IsDigit(input.Array[index])) { // ... index++; } } var arraySegment = new ArraySegment(array, from, length); var res = ParseInt((IList<char>)arraySegment); 

Sin embargo, creo que esto es demasiado tanto en términos de lógica como de disminución en el rendimiento. ArraySegment está mal diseñado y ralentiza el acceso a los elementos 7 veces más en comparación con las mismas operaciones realizadas con una matriz.


Entonces, ¿cómo resolvemos estos problemas? ¿Cómo hacemos que los desarrolladores vuelvan a usar código no administrado y les demos una herramienta unificada y rápida para trabajar con fuentes de datos heterogéneas: matrices, cadenas y memoria no administrada? Era necesario darles una sensación de confianza de que no pueden cometer un error sin saberlo. Fue necesario proporcionarles un instrumento que no disminuya los tipos de datos nativos en términos de rendimiento, pero que resuelva los problemas enumerados. Span<T> tipos Span<T> y Memory<T> son exactamente estos instrumentos.


Span <T>, ReadOnlySpan <T>


Span tipo de Span es un instrumento para trabajar con datos dentro de una sección de una matriz de datos o con un subrango de sus valores. Como en el caso de una matriz, permite leer y escribir en los elementos de este subrango, pero con una restricción importante: obtienes o creas un Span<T> solo para un trabajo temporal con una matriz, solo para llamar a un grupo de métodos . Sin embargo, para obtener una comprensión general, comparemos los tipos de datos para los que está diseñado Span y veamos sus posibles escenarios de uso.


El primer tipo de datos es una matriz habitual. Las matrices funcionan con Span de la siguiente manera:


  var array = new [] {1,2,3,4,5,6}; var span = new Span<int>(array, 1, 3); var position = span.BinarySearch(3); Console.WriteLine(span[position]); // -> 3 

Al principio, creamos una matriz de datos, como se muestra en este ejemplo. A continuación, creamos Span (o un subconjunto) que hace referencia a la matriz y hace que un rango de valores inicializados previamente sea accesible para el código que usa la matriz.


Aquí vemos la primera característica de este tipo de datos, es decir, la capacidad de crear un determinado contexto. Expandamos nuestra idea de contextos:


 void Main() { var array = new [] {'1','2','3','4','5','6'}; var span = new Span<char>(array, 1, 3); if(TryParseInt32(span, out var res)) { Console.WriteLine(res); } else { Console.WriteLine("Failed to parse"); } } public bool TryParseInt32(Span<char> input, out int result) { result = 0; for (int i = 0; i < input.Length; i++) { if(input[i] < '0' || input[i] > '9') return false; result = result * 10 + ((int)input[i] - '0'); } return true; } ----- 234 

Como vemos, Span<T> proporciona acceso abstracto a un rango de memoria tanto para lectura como para escritura. Que nos da Si recordamos para qué más podemos usar Span , pensaremos en recursos y cadenas no administrados:


 // Managed array var array = new[] { '1', '2', '3', '4', '5', '6' }; var arrSpan = new Span<char>(array, 1, 3); if (TryParseInt32(arrSpan, out var res1)) { Console.WriteLine(res1); } // String var srcString = "123456"; var strSpan = srcString.AsSpan(); if (TryParseInt32(strSpan, out var res2)) { Console.WriteLine(res2); } // void * Span<char> buf = stackalloc char[6]; buf[0] = '1'; buf[1] = '2'; buf[2] = '3'; buf[3] = '4'; buf[4] = '5'; buf[5] = '6'; if (TryParseInt32(buf, out var res3)) { Console.WriteLine(res3); } ----- 234 234 234 

Eso significa que Span<T> es una herramienta para unificar formas de trabajar con memoria, tanto administrada como no administrada. Garantiza la seguridad mientras se trabaja con dichos datos durante la recolección de basura. Es decir, si los rangos de memoria con recursos no administrados comienzan a moverse, será seguro.


Sin embargo, ¿deberíamos estar tan emocionados? ¿Podríamos lograr esto antes? Por ejemplo, en el caso de las matrices administradas, no hay duda al respecto: solo necesita envolver una matriz en una clase más (por ejemplo, [ArraySegment] existente ( https://referencesource.microsoft.com/#mscorlib/system/ arraysegment.cs, 31 )) dando una interfaz similar y eso es todo. Además, puede hacer lo mismo con las cadenas: tienen los métodos necesarios. Nuevamente, solo necesita envolver una cadena del mismo tipo y proporcionar métodos para trabajar con ella. Sin embargo, para almacenar una cadena, un búfer y una matriz en un tipo, tendrá mucho que ver con mantener referencias a cada variante posible en una sola instancia (con una sola variante activa, obviamente).


 public readonly ref struct OurSpan<T> { private T[] _array; private string _str; private T * _buffer; // ... } 

O, según la arquitectura, puede crear tres tipos que implementen una interfaz uniforme. Por lo tanto, no es posible crear una interfaz uniforme entre estos tipos de datos que sea diferente de Span<T> y mantener el máximo rendimiento.


A continuación, hay una pregunta de qué es la ref struct con respecto a Span . Estas son exactamente esas "estructuras que existen solo en la pila" de las que escuchamos durante las entrevistas de trabajo con tanta frecuencia. Significa que este tipo de datos puede asignarse solo en la pila y no puede ir al montón. Es por esto que Span , que es una estructura de referencia, es un tipo de datos de contexto que permite el trabajo de métodos pero no el de objetos en la memoria. En eso es en lo que debemos basarnos cuando tratamos de entenderlo.


Ahora podemos definir el tipo Span y el tipo ReadOnlySpan relacionado:


Span es un tipo de datos que implementa una interfaz uniforme para trabajar con tipos heterogéneos de matrices de datos y permite pasar un subconjunto de una matriz a un método para que la velocidad de acceso a la matriz original sea constante y máxima independientemente de la profundidad de la matriz. contexto.

De hecho, si tenemos un código como


 public void Method1(Span<byte> buffer) { buffer[0] = 0; Method2(buffer.Slice(1,2)); } Method2(Span<byte> buffer) { buffer[0] = 0; Method3(buffer.Slice(1,1)); } Method3(Span<byte> buffer) { buffer[0] = 0; } 

la velocidad de acceso al búfer original será la más alta a medida que trabaje con un puntero administrado y no con un objeto administrado. Eso significa que trabaja con un tipo inseguro en un contenedor administrado, pero no con un tipo administrado .NET.


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/443974/


All Articles