
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 .