Ler em inglês

Partilhar via


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.

IBufferWriter<T>

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> ou Span<T> pode ser gravado em e você pode determinar quantos T itens foram escritos.
C#
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)o IBufferWriter<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:

C#
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.

Problemas comuns do IBufferWriter

  • GetSpan e GetMemory 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ós Advance ter sido chamado.

ReadOnlySequence<T>

ReadOnlySequence showing memory in pipe and below that sequence position of read-only memory

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:

  1. Uma T[]
  2. Uma ReadOnlyMemory<T>
  3. 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>.

Aceder a dados

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:

C#
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, SequencePositionReadOnlySequence<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.

C#
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:

C#
SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data) => buffer.PositionOf(data);

Processar um ReadOnlySequence<T>

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 novo T[] na pilha.

Os exemplos a seguir demonstram alguns casos comuns de processamento ReadOnlySequence<byte>:

Processar dados binários

O exemplo a seguir analisa um comprimento inteiro big-endian de 4 bytes desde o início do ReadOnlySequence<byte>.

C#
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;
}
Processar dados de texto

O exemplo a seguir:

  • Localiza a primeira nova linha (\r\n) na ReadOnlySequence<byte> e a retorna através do parâmetro 'line' de saída.
  • Corta essa linha, excluindo o \r\n do buffer de entrada.
C#
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;
}
Segmentos vazios

É válido armazenar segmentos vazios dentro de um ReadOnlySequence<T>arquivo . Segmentos vazios podem ocorrer ao enumerar segmentos explicitamente:

C#
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 um SequencePosition 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.

Problemas potenciais com ReadOnlySequence<T> e SequencePosition

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ífica ReadOnlySequence<T>, não absoluta. Por ser relativo a um específico ReadOnlySequence<T>, não tem significado se usado fora do ReadOnlySequence<T> local de origem.
  • A aritmética não pode ser executada sem SequencePosition o ReadOnlySequence<T>. Isso significa fazer coisas básicas como position++ está escrito position = 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. Passando ReadOnlySequence<T> ou inref 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 com SequencePosition objetos.

SequenceReader<T>

SequenceReader<T>:

  • É 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 multisegmento ReadOnlySequence<T>.
  • Fornece auxiliares para a leitura de dados binários e de texto (byte e char) 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>:

Aceder a dados

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:

C#
while (reader.TryRead(out byte b))
{
    Process(b);
}

O CurrentSpan expõe o segmento Spanatual, que é semelhante ao que foi feito no método manualmente.

Posição de utilização

O código a seguir é um exemplo de implementação do FindIndexOf uso do SequenceReader<T>:

C#
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;
}

Processar dados binários

O exemplo a seguir analisa um comprimento inteiro big-endian de 4 bytes desde o início do ReadOnlySequence<byte>.

C#
bool TryParseHeaderLength(ref ReadOnlySequence<byte> buffer, out int length)
{
    var reader = new SequenceReader<byte>(buffer);
    return reader.TryReadBigEndian(out length);
}

Processar dados de texto

C#
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;
}

Problemas comuns do SequenceReader<T>

  • 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 outros Read, Peeke IsNext APIs.