Memória e Span pt. 2


Span <T> exemplos de uso


Um humano por natureza não pode entender completamente o propósito de um determinado instrumento até que ele ou ela obtenha alguma experiência. Então, vamos ver alguns exemplos.


ValueStringBuilder


Um dos exemplos mais interessantes em relação aos algoritmos é o tipo ValueStringBuilder . No entanto, ele está enterrado profundamente no mscorlib e marcado com o modificador internal como muitos outros tipos de dados muito interessantes. Isso significa que não encontraríamos esse instrumento notável para otimização se não tivéssemos pesquisado o código fonte do mscorlib.


Qual é a principal desvantagem do tipo de sistema StringBuilder ? Sua principal desvantagem é o tipo e sua base - é um tipo de referência e é baseado em char[] , ou seja, uma matriz de caracteres. Pelo menos, isso significa duas coisas: usamos o heap (embora não muito) e aumentamos as chances de perder o dinheiro da CPU.


Outro problema com o StringBuilder que eu enfrentei é a construção de cadeias pequenas, ou seja, quando a cadeia resultante deve ser curta, por exemplo, menos de 100 caracteres. A formatação curta levanta problemas de desempenho.


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 .

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

Até que ponto essa variante é pior que a construção manual por meio do StringBuilder ? A resposta nem sempre é óbvia. Depende do local da construção e da frequência de chamada desse método. Inicialmente, string.Format aloca memória para StringBuilder interno que criará uma matriz de caracteres (SourceString.Length + args.Length * 8). Se durante a construção da matriz ocorrer que o comprimento foi determinado incorretamente, outro StringBuilder será criado para construir o restante. Isso levará à criação de uma única lista vinculada. Como resultado, ele deve retornar a string construída, o que significa outra cópia. Isso é um desperdício. Seria ótimo se pudéssemos nos livrar da alocação da matriz de uma string formada na pilha: isso resolveria um dos nossos problemas.


Vejamos esse tipo a partir da profundidade do 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); } 

Essa classe é funcionalmente semelhante ao seu colega sênior StringBuilder , embora tenha um recurso interessante e muito importante: é um tipo de valor. Isso significa que ele é armazenado e transmitido inteiramente por valor. Além disso, um novo modificador de tipo ref , que faz parte de uma assinatura de declaração de tipo, indica que esse tipo tem uma restrição adicional: ele pode ser alocado apenas na pilha. Quero dizer, passar suas instâncias para os campos de classe produzirá um erro. Para que serve tudo isso? Para responder a essa pergunta, basta olhar para a classe StringBuilder , cuja essência acabamos de descrever:


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 é uma classe que contém uma referência a uma matriz de caracteres. Portanto, quando você o cria, aparecem dois objetos: StringBuilder e uma matriz de caracteres com pelo menos 16 caracteres. É por isso que é essencial definir o comprimento esperado de uma string: ela será criada gerando uma única lista vinculada de matrizes com 16 caracteres cada. Admita, isso é um desperdício. Em termos do tipo ValueStringBuilder , isso significa que não há capacity padrão, pois empresta memória externa. Além disso, é um tipo de valor e faz com que o usuário aloque um buffer para caracteres na pilha. Assim, toda a instância de um tipo é colocada na pilha junto com seu conteúdo e a questão da otimização é resolvida. Como não há necessidade de alocar memória no heap, não há problemas com uma diminuição no desempenho ao lidar com o heap. Portanto, você pode ter uma pergunta: por que nem sempre usamos o ValueStringBuilder (ou seu analógico personalizado, pois não podemos usar o original porque ele é interno)? A resposta é: depende de uma tarefa. Uma sequência resultante terá um tamanho definido? Terá um comprimento máximo conhecido? Se você responder "yes" e se a string não exceder os limites razoáveis, poderá usar a versão de valor do StringBuilder . No entanto, se você espera seqüências longas, use a versão usual.


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

O segundo tipo de dados que quero observar especialmente é o tipo ValueListBuilder . É usado quando você precisa criar uma coleção de elementos por um curto período de tempo e passá-lo para um algoritmo para processamento.


Admita, esta tarefa é bem parecida com a tarefa ValueStringBuilder . E é resolvido de maneira semelhante:


Arquivo ValueListBuilder.cs coreclr / src /../ Generic / ValueListBuilder.cs


Para colocar claramente, essas situações são frequentemente. No entanto, anteriormente resolvemos o problema de outra maneira. Costumávamos criar uma List , preenchê-la com dados e perder a referência a ela. Se o método for chamado com freqüência, isso levará a uma situação triste: muitas instâncias da List (e matrizes associadas) são suspensas no heap. Agora esse problema está resolvido: nenhum objeto adicional será criado. No entanto, como no caso do ValueStringBuilder ele é resolvido apenas para programadores da Microsoft: essa classe possui o modificador internal .


Regras e prática de uso


Para entender completamente o novo tipo de dados, você precisa jogar com ele escrevendo dois ou três ou mais métodos que o utilizam. No entanto, é possível aprender as principais regras agora:


  • Se o seu método processar algum conjunto de dados de entrada sem alterar seu tamanho, você pode tentar manter o tipo Span . Se você não deseja modificar o buffer, escolha o tipo ReadOnlySpan ;
  • Se o seu método manipular cadeias de caracteres calculando algumas estatísticas ou analisando essas cadeias, ele deverá aceitar ReadOnlySpan<char> . Must é uma nova regra. Porque quando você aceita uma string, você faz alguém criar uma substring para você;
  • Se você precisar criar uma matriz de dados curta (não mais que 10 kB) para um método, poderá organizá-la facilmente usando Span<TType> buf = stackalloc TType[size] . Observe que TType deve ser um tipo de valor, pois stackalloc trabalha apenas com tipos de valor.

Em outros casos, é melhor examinar mais de perto a Memory ou usar tipos de dados clássicos.


Como o span funciona?


Gostaria de dizer algumas palavras adicionais sobre como o Span funciona e por que isso é notável. E há algo para se falar. Esse tipo de dados possui duas versões: uma para o .NET Core 2.0+ e outra para o restante.


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

Arquivo ??? [descompilado]


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

O importante é que o .NET Framework enorme e o .NET Core 1. * não têm um coletor de lixo atualizado de maneira especial (diferente do .NET Core 2.0+) e precisam usar um ponteiro adicional para o início de um buffer no use. Isso significa que o Span internamente manipula objetos .NET gerenciados como se não fossem gerenciados. Basta olhar para a segunda variante da estrutura: ela possui três campos. O primeiro é uma referência a um objeto gerenciado. O segundo é o deslocamento em bytes desde o início deste objeto, usado para definir o início do buffer de dados (em cadeias esse buffer contém caracteres char , enquanto que em matrizes contém os dados de uma matriz). Finalmente, o terceiro campo contém a quantidade de elementos no buffer dispostos em uma linha.


Vamos ver como o Span lida com seqüências de caracteres, por exemplo:


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

Onde string.GetRawStringData() tem a seguinte aparência:


Um arquivo com a definição dos campos coreclr /../ System / String.CoreCLR.cs


Um arquivo com a definição 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; } 

Acontece que o método acessa diretamente o interior da sequência, enquanto a especificação ref char permite que o GC rastreie uma referência não gerenciada àquela dentro da sequência, movendo-a junto com a sequência quando o GC está ativo.


O mesmo ocorre com matrizes: quando Span é criado, algum código JIT interno calcula o deslocamento para o início da matriz de dados e inicializa o Span com esse deslocamento. A maneira de calcular o deslocamento de seqüências de caracteres e matrizes foi discutida no capítulo sobre a estrutura dos objetos na memória (. \ ObjectsStructure.md).


Span <T> como um valor retornado


Apesar de toda a harmonia, Span tem algumas restrições lógicas, mas inesperadas, ao retornar de um método. Se olharmos para o seguinte código:


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

podemos ver que é lógico e bom. No entanto, se substituirmos uma instrução por outra:


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

um compilador o proibirá. Antes de dizer o porquê, gostaria que você adivinhasse quais problemas esse construto traz.


Bem, espero que você tenha pensado, adivinhou e talvez até tenha entendido o motivo. Se sim, meus esforços para escrever um capítulo detalhado sobre uma [pilha de threads] (./ThreadStack.md) foram recompensados. Como quando você retorna uma referência a variáveis ​​locais de um método que termina seu trabalho, você pode chamar outro método, esperar até que ele termine seu trabalho e ler os valores dessas variáveis ​​locais usando x [0,99].


Felizmente, quando tentamos escrever esse código, um compilador dá um tapinha nos pulsos, alertando: CS8352 Cannot use local 'reff' in this context because it may expose referenced variables outside of their declaration scope . O compilador está certo porque, se você ignorar esse erro, haverá uma chance, enquanto estiver em um plug-in, de roubar as senhas de outras pessoas ou de elevar privilégios para executar nosso plug-in.


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


All Articles