Trabalhar com buffers no .NET
Este artigo fornece uma visão geral dos tipos que ajudam a ler dados executados em vários buffers. Eles são usados principalmente para suportar PipeReader objetos.
System.Buffers.IBufferWriter<T> é um contrato para gravação em buffer síncrona. No nível mais baixo, a interface:
- É básico e não é difícil de usar.
- Permite o acesso a um Memory<T> ou Span<T>. O
Memory<T>
ouSpan<T>
pode ser gravado em e você pode determinar quantosT
itens foram escritos.
void WriteHello(IBufferWriter<byte> writer)
{
// Request at least 5 bytes.
Span<byte> span = writer.GetSpan(5);
ReadOnlySpan<char> helloSpan = "Hello".AsSpan();
int written = Encoding.ASCII.GetBytes(helloSpan, span);
// Tell the writer how many bytes were written.
writer.Advance(written);
}
O método anterior:
- Solicita um buffer de pelo menos 5 bytes do usando
GetSpan(5)
oIBufferWriter<byte>
. - Grava bytes para a cadeia de caracteres ASCII "Olá" para o retornado
Span<byte>
. - Chamadas IBufferWriter<T> para indicar quantos bytes foram gravados no buffer.
Este método de escrita usa o Memory<T>
/Span<T>
buffer fornecido pelo IBufferWriter<T>
. Como alternativa, o Write método de extensão pode ser usado para copiar um buffer existente para o IBufferWriter<T>
. Write
faz o trabalho de ligar GetSpan
/Advance
conforme apropriado, então não há necessidade de ligar Advance
depois de escrever:
void WriteHello(IBufferWriter<byte> writer)
{
byte[] helloBytes = Encoding.ASCII.GetBytes("Hello");
// Write helloBytes to the writer. There's no need to call Advance here
// since Write calls Advance.
writer.Write(helloBytes);
}
ArrayBufferWriter<T> é uma implementação cujo armazenamento de IBufferWriter<T>
suporte é uma única matriz contígua.
GetSpan
eGetMemory
retornar um buffer com pelo menos a quantidade solicitada de memória. Não assuma tamanhos exatos de buffer.- Não há garantia de que chamadas sucessivas retornarão o mesmo buffer ou o mesmo buffer de tamanho.
- Um novo buffer deve ser solicitado após a chamada
Advance
para continuar gravando mais dados. Um buffer adquirido anteriormente não pode ser gravado apósAdvance
ter sido chamado.
ReadOnlySequence<T> é uma estrutura que pode representar uma sequência contígua ou não contígua de T
. Pode ser construído a partir de:
- Uma
T[]
- Uma
ReadOnlyMemory<T>
- Um par de nó ReadOnlySequenceSegment<T> de lista vinculada e índice para representar a posição inicial e final da sequência.
A terceira representação é a mais interessante, pois tem implicações de desempenho em várias operações no ReadOnlySequence<T>
:
Representação | Operação | Complexidade |
---|---|---|
T[] /ReadOnlyMemory<T> |
Length |
O(1) |
T[] /ReadOnlyMemory<T> |
GetPosition(long) |
O(1) |
T[] /ReadOnlyMemory<T> |
Slice(int, int) |
O(1) |
T[] /ReadOnlyMemory<T> |
Slice(SequencePosition, SequencePosition) |
O(1) |
ReadOnlySequenceSegment<T> |
Length |
O(1) |
ReadOnlySequenceSegment<T> |
GetPosition(long) |
O(number of segments) |
ReadOnlySequenceSegment<T> |
Slice(int, int) |
O(number of segments) |
ReadOnlySequenceSegment<T> |
Slice(SequencePosition, SequencePosition) |
O(1) |
Devido a essa representação mista, o ReadOnlySequence<T>
expõe índices como SequencePosition
em vez de um inteiro. A SequencePosition
:
- É um valor opaco que representa um índice no
ReadOnlySequence<T>
local onde se originou. - Consiste em duas partes, um inteiro e um objeto. O que estes dois valores representam está ligado à implementação do
ReadOnlySequence<T>
.
O ReadOnlySequence<T>
expõe dados como um enumerável de ReadOnlyMemory<T>
. A enumeração de cada um dos segmentos pode ser feita usando uma pregação básica:
long FindIndexOf(in ReadOnlySequence<byte> buffer, byte data)
{
long position = 0;
foreach (ReadOnlyMemory<byte> segment in buffer)
{
ReadOnlySpan<byte> span = segment.Span;
var index = span.IndexOf(data);
if (index != -1)
{
return position + index;
}
position += span.Length;
}
return -1;
}
O método anterior pesquisa cada segmento para um byte específico. Se você precisa acompanhar o de cada segmento, SequencePosition
ReadOnlySequence<T>.TryGet é mais adequado. O próximo exemplo altera o código anterior para retornar um SequencePosition
em vez de um inteiro. Retornar um SequencePosition
tem a vantagem de permitir que o chamador evite uma segunda varredura para obter os dados em um índice específico.
SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data)
{
SequencePosition position = buffer.Start;
SequencePosition result = position;
while (buffer.TryGet(ref position, out ReadOnlyMemory<byte> segment))
{
ReadOnlySpan<byte> span = segment.Span;
var index = span.IndexOf(data);
if (index != -1)
{
return buffer.GetPosition(index, result);
}
result = position;
}
return null;
}
A combinação de SequencePosition
e TryGet
age como um enumerador. O campo de posição é modificado no início de cada iteração para ser o início de cada segmento dentro do ReadOnlySequence<T>
.
O método anterior existe como um método de extensão em ReadOnlySequence<T>
. PositionOf pode ser usado para simplificar o código anterior:
SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data) => buffer.PositionOf(data);
O processamento de um ReadOnlySequence<T>
pode ser um desafio, uma vez que os dados podem ser divididos em vários segmentos dentro da sequência. Para obter o melhor desempenho, divida o código em dois caminhos:
- Um caminho rápido que lida com o caso de segmento único.
- Um caminho lento que lida com os dados divididos entre segmentos.
Existem algumas abordagens que podem ser usadas para processar dados em sequências multissegmentadas:
- Utilize a seringa
SequenceReader<T>
. - Analise os dados segmento por segmento, acompanhando o índice e dentro
SequencePosition
do segmento analisado. Isso evita alocações desnecessárias, mas pode ser ineficiente, especialmente para buffers pequenos. - Copie o para uma matriz contígua e trate-o
ReadOnlySequence<T>
como um único buffer:- Se o tamanho do
ReadOnlySequence<T>
for pequeno, pode ser razoável copiar os dados em um buffer alocado por pilha usando o operador stackalloc . - Copie o
ReadOnlySequence<T>
para uma matriz em pool usando ArrayPool<T>.Sharedo . ReadOnlySequence<T>.ToArray()
Utilize. Isso não é recomendado em caminhos quentes, pois aloca um novoT[]
na pilha.
- Se o tamanho do
Os exemplos a seguir demonstram alguns casos comuns de processamento ReadOnlySequence<byte>
:
O exemplo a seguir analisa um comprimento inteiro big-endian de 4 bytes desde o início do ReadOnlySequence<byte>
.
bool TryParseHeaderLength(ref ReadOnlySequence<byte> buffer, out int length)
{
// If there's not enough space, the length can't be obtained.
if (buffer.Length < 4)
{
length = 0;
return false;
}
// Grab the first 4 bytes of the buffer.
var lengthSlice = buffer.Slice(buffer.Start, 4);
if (lengthSlice.IsSingleSegment)
{
// Fast path since it's a single segment.
length = BinaryPrimitives.ReadInt32BigEndian(lengthSlice.First.Span);
}
else
{
// There are 4 bytes split across multiple segments. Since it's so small, it
// can be copied to a stack allocated buffer. This avoids a heap allocation.
Span<byte> stackBuffer = stackalloc byte[4];
lengthSlice.CopyTo(stackBuffer);
length = BinaryPrimitives.ReadInt32BigEndian(stackBuffer);
}
// Move the buffer 4 bytes ahead.
buffer = buffer.Slice(lengthSlice.End);
return true;
}
O exemplo a seguir:
- Localiza a primeira nova linha (
\r\n
) naReadOnlySequence<byte>
e a retorna através do parâmetro 'line' de saída. - Corta essa linha, excluindo o
\r\n
do buffer de entrada.
static bool TryParseLine(ref ReadOnlySequence<byte> buffer, out ReadOnlySequence<byte> line)
{
SequencePosition position = buffer.Start;
SequencePosition previous = position;
var index = -1;
line = default;
while (buffer.TryGet(ref position, out ReadOnlyMemory<byte> segment))
{
ReadOnlySpan<byte> span = segment.Span;
// Look for \r in the current segment.
index = span.IndexOf((byte)'\r');
if (index != -1)
{
// Check next segment for \n.
if (index + 1 >= span.Length)
{
var next = position;
if (!buffer.TryGet(ref next, out ReadOnlyMemory<byte> nextSegment))
{
// You're at the end of the sequence.
return false;
}
else if (nextSegment.Span[0] == (byte)'\n')
{
// A match was found.
break;
}
}
// Check the current segment of \n.
else if (span[index + 1] == (byte)'\n')
{
// It was found.
break;
}
}
previous = position;
}
if (index != -1)
{
// Get the position just before the \r\n.
var delimeter = buffer.GetPosition(index, previous);
// Slice the line (excluding \r\n).
line = buffer.Slice(buffer.Start, delimeter);
// Slice the buffer to get the remaining data after the line.
buffer = buffer.Slice(buffer.GetPosition(2, delimeter));
return true;
}
return false;
}
É válido armazenar segmentos vazios dentro de um ReadOnlySequence<T>
arquivo . Segmentos vazios podem ocorrer ao enumerar segmentos explicitamente:
static void EmptySegments()
{
// This logic creates a ReadOnlySequence<byte> with 4 segments,
// two of which are empty.
var first = new BufferSegment(new byte[0]);
var last = first.Append(new byte[] { 97 })
.Append(new byte[0]).Append(new byte[] { 98 });
// Construct the ReadOnlySequence<byte> from the linked list segments.
var data = new ReadOnlySequence<byte>(first, 0, last, 1);
// Slice using numbers.
var sequence1 = data.Slice(0, 2);
// Slice using SequencePosition pointing at the empty segment.
var sequence2 = data.Slice(data.Start, 2);
Console.WriteLine($"sequence1.Length={sequence1.Length}"); // sequence1.Length=2
Console.WriteLine($"sequence2.Length={sequence2.Length}"); // sequence2.Length=2
// sequence1.FirstSpan.Length=1
Console.WriteLine($"sequence1.FirstSpan.Length={sequence1.FirstSpan.Length}");
// Slicing using SequencePosition will Slice the ReadOnlySequence<byte> directly
// on the empty segment!
// sequence2.FirstSpan.Length=0
Console.WriteLine($"sequence2.FirstSpan.Length={sequence2.FirstSpan.Length}");
// The following code prints 0, 1, 0, 1.
SequencePosition position = data.Start;
while (data.TryGet(ref position, out ReadOnlyMemory<byte> memory))
{
Console.WriteLine(memory.Length);
}
}
class BufferSegment : ReadOnlySequenceSegment<byte>
{
public BufferSegment(Memory<byte> memory)
{
Memory = memory;
}
public BufferSegment Append(Memory<byte> memory)
{
var segment = new BufferSegment(memory)
{
RunningIndex = RunningIndex + Memory.Length
};
Next = segment;
return segment;
}
}
O código anterior cria um ReadOnlySequence<byte>
com segmentos vazios e mostra como esses segmentos vazios afetam as várias APIs:
ReadOnlySequence<T>.Slice
com umSequencePosition
apontamento para um segmento vazio preserva esse segmento.ReadOnlySequence<T>.Slice
com um int ignora os segmentos vazios.- Enumerar o
ReadOnlySequence<T>
enumera os segmentos vazios.
Existem vários resultados incomuns quando se lida com um ReadOnlySequence<T>
/SequencePosition
vs. um normal:ReadOnlySpan<T>
/ReadOnlyMemory<T>
/T[]
/int
SequencePosition
é um marcador de posição para uma posição específicaReadOnlySequence<T>
, não absoluta. Por ser relativo a um específicoReadOnlySequence<T>
, não tem significado se usado fora doReadOnlySequence<T>
local de origem.- A aritmética não pode ser executada sem
SequencePosition
oReadOnlySequence<T>
. Isso significa fazer coisas básicas comoposition++
está escritoposition = ReadOnlySequence<T>.GetPosition(1, position)
. GetPosition(long)
não suporta índices negativos. Isso significa que é impossível obter o penúltimo personagem sem percorrer todos os segmentos.- Dois
SequencePosition
não podem ser comparados, o que dificulta:- Saiba se uma posição é maior ou menor que outra.
- Escreva alguns algoritmos de análise.
ReadOnlySequence<T>
é maior do que uma referência de objeto e deve ser passada por in ou ref sempre que possível. PassandoReadOnlySequence<T>
ouin
ref
reduzindo cópias da estrutura.- Segmentos vazios:
- São válidos dentro de um
ReadOnlySequence<T>
arquivo . - Pode aparecer ao iterar usando o
ReadOnlySequence<T>.TryGet
método. - Pode aparecer fatiando a sequência usando o
ReadOnlySequence<T>.Slice()
método comSequencePosition
objetos.
- São válidos dentro de um
- É um novo tipo que foi introduzido no .NET Core 3.0 para simplificar o processamento de um
ReadOnlySequence<T>
arquivo . - Unifica as diferenças entre um único segmento
ReadOnlySequence<T>
e um multisegmentoReadOnlySequence<T>
. - Fornece auxiliares para a leitura de dados binários e de texto (
byte
echar
) que podem ou não ser divididos entre segmentos.
Existem métodos internos para lidar com o processamento de dados binários e delimitados. A seção a seguir demonstra como esses mesmos métodos se parecem com o SequenceReader<T>
:
SequenceReader<T>
tem métodos para enumerar dados dentro do ReadOnlySequence<T>
diretamente. O código a seguir é um exemplo de processamento de um ReadOnlySequence<byte>
a byte
de cada vez:
while (reader.TryRead(out byte b))
{
Process(b);
}
O CurrentSpan
expõe o segmento Span
atual, que é semelhante ao que foi feito no método manualmente.
O código a seguir é um exemplo de implementação do FindIndexOf
uso do SequenceReader<T>
:
SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data)
{
var reader = new SequenceReader<byte>(buffer);
while (!reader.End)
{
// Search for the byte in the current span.
var index = reader.CurrentSpan.IndexOf(data);
if (index != -1)
{
// It was found, so advance to the position.
reader.Advance(index);
return reader.Position;
}
// Skip the current segment since there's nothing in it.
reader.Advance(reader.CurrentSpan.Length);
}
return null;
}
O exemplo a seguir analisa um comprimento inteiro big-endian de 4 bytes desde o início do ReadOnlySequence<byte>
.
bool TryParseHeaderLength(ref ReadOnlySequence<byte> buffer, out int length)
{
var reader = new SequenceReader<byte>(buffer);
return reader.TryReadBigEndian(out length);
}
static ReadOnlySpan<byte> NewLine => new byte[] { (byte)'\r', (byte)'\n' };
static bool TryParseLine(ref ReadOnlySequence<byte> buffer,
out ReadOnlySequence<byte> line)
{
var reader = new SequenceReader<byte>(buffer);
if (reader.TryReadTo(out line, NewLine))
{
buffer = buffer.Slice(reader.Position);
return true;
}
line = default;
return false;
}
- Por
SequenceReader<T>
ser uma estrutura mutável, ela deve sempre ser passada por referência. SequenceReader<T>
é uma estrutura ref, portanto, só pode ser usada em métodos síncronos e não pode ser armazenada em campos. Para obter mais informações, consulte Evitar alocações.SequenceReader<T>
é otimizado para uso como um leitor somente para encaminhamento.Rewind
destina-se a pequenos backups que não podem ser resolvidos utilizando outrosRead
,Peek
eIsNext
APIs.
Comentários do .NET
O .NET é um projeto código aberto. Selecione um link para fornecer comentários: