Memória e Span pt. 1

A partir do .NET Core 2.0 e .NET Framework 4.5, podemos usar novos tipos de dados: Span e Memory . Para usá-los, você só precisa instalar o pacote de nuget System.Memory :


PM> Install-Package System.Memory

Esses tipos de dados são notáveis ​​porque a equipe do CLR fez um ótimo trabalho para implementar seu suporte especial dentro do código do compilador JIT do .NET Core 2.1+, incorporando esses tipos de dados diretamente no núcleo. Que tipo de dados são esses e por que eles valem um capítulo inteiro?


Se falarmos sobre problemas que fizeram esses tipos aparecerem, devo citar três deles. O primeiro é um código não gerenciado.


O idioma e a plataforma existem há muitos anos, juntamente com os meios para trabalhar com código não gerenciado. Então, por que lançar outra API para trabalhar com código não gerenciado, se a primeira existir basicamente por muitos anos? Para responder a essa pergunta, devemos entender o que nos faltava antes.


Este capítulo foi traduzido do russo em conjunto pelo autor e por tradutores profissionais . Você pode nos ajudar com a tradução do russo ou do inglês para qualquer outro idioma, principalmente para chinês ou alemão.

Além disso, se você quiser nos agradecer, a melhor maneira de fazer isso é nos dar uma estrela no github ou no fork do repositório github / sidristij / dotnetbook .

Os desenvolvedores da plataforma já tentaram facilitar o uso de recursos não gerenciados para nós. Eles implementaram invólucros automáticos para métodos importados e empacotamento que funcionam automaticamente na maioria dos casos. Aqui também pertence ao stackalloc , mencionado no capítulo sobre uma pilha de threads. No entanto, a meu ver, os primeiros desenvolvedores de C # vieram do mundo C ++ (meu caso), mas agora eles mudam de linguagens de mais alto nível (conheço um desenvolvedor que já escreveu em JavaScript). Isso significa que as pessoas estão ficando mais desconfiadas com o código não gerenciado e as construções C / C +, e muito mais com o Assembler.


Como resultado, os projetos contêm cada vez menos código inseguro e a confiança na API da plataforma aumenta cada vez mais. É fácil verificar se procuramos por casos de uso stackalloc em repositórios públicos - eles são escassos. No entanto, vamos usar qualquer código que o utilize:


Classe 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 que não é popular. Basta procurar este código e questionar-se se você confia nele. Eu acho que a resposta é 'Não'. Então, pergunte-se o porquê. É óbvio: não apenas vemos a palavra Dangerous , o que sugere que algo pode dar errado, mas há a palavra-chave unsafe e byte* buffer = stackalloc byte[s_readBufferSize]; linha (especificamente - byte* ) que muda nossa atitude. Este é um gatilho para você pensar: "Não havia outra maneira de fazer isso"? Então, vamos nos aprofundar na psicanálise: por que você pensa assim? Por um lado, usamos construções de linguagem e a sintaxe oferecida aqui está longe de, por exemplo, C ++ / CLI, que permite qualquer coisa (até mesmo a inserção de código Assembler puro). Por outro lado, essa sintaxe parece incomum.


O segundo problema que os desenvolvedores consideram implícita ou explicitamente é a incompatibilidade dos tipos string e char []. Embora, logicamente, uma string seja uma matriz de caracteres, mas não é possível converter uma string para char []: você só pode criar um novo objeto e copiar o conteúdo de uma string para uma matriz. Essa incompatibilidade é introduzida para otimizar seqüências de caracteres em termos de armazenamento (não há matrizes somente leitura). No entanto, problemas aparecem quando você começa a trabalhar com arquivos. Como lê-los? Como uma string ou como uma matriz? Se você escolher uma matriz, não poderá usar alguns métodos criados para trabalhar com seqüências de caracteres. Que tal ler como uma string? Pode demorar demais. Se você precisar analisá-lo, qual analisador deve escolher para tipos de dados primitivos: nem sempre você deseja analisá-los manualmente (números inteiros, flutuantes, fornecidos em diferentes formatos). Temos muitos algoritmos comprovados que fazem isso de maneira mais rápida e eficiente, não é? No entanto, esses algoritmos geralmente trabalham com seqüências de caracteres que não contêm nada além de um tipo primitivo. Então, há um dilema.


O terceiro problema é que os dados exigidos por um algoritmo raramente fazem uma fatia de dados contínua e sólida dentro de uma seção de uma matriz, lida a partir de alguma fonte. Por exemplo, no caso de arquivos ou dados lidos de um soquete, temos parte dos que já foram processados ​​por um algoritmo, seguidos por uma parte dos dados que devem ser processados ​​pelo nosso método e, em seguida, por dados ainda não processados. Idealmente, nosso método deseja apenas os dados para os quais esse método foi projetado. Por exemplo, um método que analisa números inteiros não ficará satisfeito com uma sequência que contém algumas palavras com um número esperado em algum lugar entre elas. Este método quer um número e nada mais. Ou, se passarmos uma matriz inteira, é necessário indicar, por exemplo, o deslocamento de um número desde o início da matriz.


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

No entanto, essa abordagem é ruim, pois esse método obtém dados desnecessários. Em outras palavras, o método é chamado para contextos para os quais não foi projetado e precisa resolver algumas tarefas externas. Este é um design ruim. Como evitar esses problemas? Como opção, podemos usar o tipo ArraySegment<T> que pode dar acesso a uma seção de uma 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); 

No entanto, acho que isso é demais, tanto em termos de lógica quanto em uma diminuição no desempenho. ArraySegment é mal projetado e diminui o acesso aos elementos 7 vezes mais em comparação com as mesmas operações feitas com uma matriz.


Então, como resolvemos esses problemas? Como fazemos com que os desenvolvedores voltem a usar código não gerenciado e forneçam a eles uma ferramenta rápida e unificada para trabalhar com fontes de dados heterogêneas: matrizes, strings e memória não gerenciada. Era necessário dar a eles uma sensação de confiança de que eles não podem cometer um erro sem saber. Era necessário fornecer a eles um instrumento que não diminuísse os tipos de dados nativos em termos de desempenho, mas resolvesse os problemas listados. Span<T> tipos Span<T> e Memory<T> são exatamente esses instrumentos.


Extensão <T>, ReadOnlySpan <T>


Span tipo Span é um instrumento para trabalhar com dados dentro de uma seção de uma matriz de dados ou com um subintervalo de seus valores. Como no caso de um array, ele permite a leitura e a gravação dos elementos desse subintervalo, mas com uma restrição importante: você obtém ou cria um Span<T> apenas para um trabalho temporário com um array, apenas para chamar um grupo de métodos . No entanto, para obter um entendimento geral, vamos comparar os tipos de dados para os quais o Span foi projetado e examinar os possíveis cenários de uso.


O primeiro tipo de dados é uma matriz usual. As matrizes funcionam com o Span da seguinte maneira:


  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 

Inicialmente, criamos uma matriz de dados, conforme mostrado neste exemplo. Em seguida, criamos Span (ou um subconjunto) que faz referência à matriz e torna um intervalo de valores inicializado anteriormente acessível ao código que usa a matriz.


Aqui vemos a primeira característica desse tipo de dados, ou seja, a capacidade de criar um determinado contexto. Vamos expandir nossa ideia 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> fornece acesso abstrato a um intervalo de memória, tanto para leitura quanto para gravação. O que isso nos dá? Se lembrarmos para o que mais podemos usar o Span , pensaremos em recursos e strings não gerenciados:


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

Isso significa que o Span<T> é uma ferramenta para unificar maneiras de trabalhar com a memória, gerenciada e não gerenciada. Garante segurança ao trabalhar com esses dados durante a coleta de lixo. Ou seja, se os intervalos de memória com recursos não gerenciados começarem a se mover, será seguro.


No entanto, devemos estar tão animados? Poderíamos conseguir isso antes? Por exemplo, no caso de matrizes gerenciadas, não há dúvida: basta agrupar uma matriz em mais uma classe (por exemplo, [ArraySegment] de longa data ( https://referencesource.microsoft.com/#mscorlib/system/ arraysegment.cs, 31 )) dando assim uma interface semelhante e é isso. Além disso, você pode fazer o mesmo com as strings - elas possuem métodos necessários. Novamente, você só precisa agrupar uma sequência do mesmo tipo e fornecer métodos para trabalhar com ela. No entanto, para armazenar uma string, um buffer e uma matriz em um tipo, você terá muito a ver com a manutenção de referências a cada variante possível em uma única instância (com apenas uma variante ativa, obviamente).


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

Ou, com base na arquitetura, você pode criar três tipos que implementam uma interface uniforme. Portanto, não é possível criar uma interface uniforme entre esses tipos de dados que seja diferente do Span<T> e manter o desempenho máximo.


Em seguida, há uma pergunta sobre o que é ref struct em relação ao Span ? Essas são exatamente aquelas “estruturas existentes apenas na pilha” sobre as quais ouvimos muitas vezes durante entrevistas de emprego. Isso significa que esse tipo de dados pode ser alocado apenas na pilha e não pode ir para o heap. É por isso que Span , que é uma estrutura ref, é um tipo de dados de contexto que permite o trabalho de métodos, mas não o de objetos na memória. É nisso que precisamos nos basear ao tentar entendê-lo.


Agora podemos definir o tipo Span e o tipo ReadOnlySpan relacionado:


Span é um tipo de dados que implementa uma interface uniforme para trabalhar com tipos heterogêneos de matrizes de dados e permite passar um subconjunto de uma matriz para um método para que a velocidade de acesso à matriz original seja constante e mais alta, independentemente da profundidade do contexto.

De fato, se tivermos um 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; } 

a velocidade de acesso ao buffer original será a mais alta à medida que você trabalha com um ponteiro gerenciado e não com um objeto gerenciado. Isso significa que você trabalha com um tipo não seguro em um wrapper gerenciado, mas não com um tipo gerenciado .NET.


Este capítulo foi traduzido do russo em conjunto pelo autor e por tradutores profissionais . Você pode nos ajudar com a tradução do russo ou do inglês para qualquer outro idioma, principalmente para chinês ou alemão.

Além disso, se você quiser nos agradecer, a melhor maneira de fazer isso é nos dar uma estrela no github ou no fork do repositório github / sidristij / dotnetbook .

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


All Articles