Leer en inglés

Compartir a través de


Trabajo con búferes en .NET

En este artículo se proporciona información general sobre los tipos que ayudan a leer datos que se ejecutan en varios búferes. Se usan principalmente para la compatibilidad con objetos PipeReader.

IBufferWriter<T>

System.Buffers.IBufferWriter<T> es un contrato para la escritura sincrónica en búfer. En el nivel más bajo, la interfaz:

  • Es básica y fácil de usar.
  • Permite el acceso a Memory<T> o Span<T>. Es posible escribir en Memory<T> o Span<T> y se puede determinar cuántos elementos T se escribieron.
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);
}

El método anterior:

  • Solicita un búfer de al menos 5 bytes de IBufferWriter<byte> mediante GetSpan(5).
  • Escribe los bytes de la cadena ASCII "Hola" en el elemento Span<byte> devuelto.
  • Llama a IBufferWriter<T> para indicar el número de bytes que se han escrito en el búfer.

Este método de escritura usa el búfer Memory<T>/Span<T> proporcionado por IBufferWriter<T>. Como alternativa, se puede usar el método de extensión Write para copiar un búfer existente en IBufferWriter<T>. Write realiza el trabajo de llamar a GetSpan/Advance según sea necesario, por lo que no hace falta llamar a Advance después de escribir:

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> es una implementación de IBufferWriter<T> cuya memoria auxiliar es una sola matriz contigua.

Problemas comunes de IBufferWriter

  • GetSpan y GetMemory devuelven un búfer con, al menos, la cantidad de memoria solicitada. No asume tamaños de búfer exactos.
  • No existe ninguna garantía de que las llamadas sucesivas devuelvan el mismo búfer o el mismo tamaño del búfer.
  • Se debe solicitar un nuevo búfer después de llamar a Advance para seguir escribiendo más datos. No se puede escribir en un búfer adquirido previamente después de llamar a Advance.

ReadOnlySequence<T>

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

ReadOnlySequence<T> es una estructura que puede representar una secuencia de T contigua o no contigua. Se puede construir a partir de:

  1. T[].
  2. ReadOnlyMemory<T>.
  3. Un par de nodos de lista vinculados ReadOnlySequenceSegment<T> y el índice para representar la posición inicial y final de la secuencia.

La tercera representación es la más interesante, ya que tiene implicaciones para el rendimiento en varias operaciones en ReadOnlySequence<T>:

Representación Operación Complejidad
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)

Debido a esta representación mixta, ReadOnlySequence<T> expone los índices como SequencePosition en lugar de un entero. SequencePosition:

  • es un valor opaco que representa un índice en la estructura ReadOnlySequence<T> donde se originó.
  • Consta de dos partes: un entero y un objeto. Estos dos valores representan que están asociados a la implementación de ReadOnlySequence<T>.

Acceder a datos

ReadOnlySequence<T> expone los datos como un valor enumerable de ReadOnlyMemory<T>. La enumeración de cada uno de los segmentos puede realizarse mediante una instrucción foreach 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;
}

El método anterior busca un byte específico en cada segmento. Si necesita realizar un seguimiento del valor SequencePosition de cada segmento, es más adecuado ReadOnlySequence<T>.TryGet. En el ejemplo siguiente se cambia el código anterior para devolver SequencePosition en lugar de un entero. Devolver SequencePosition tiene la ventaja de permitir que el autor de la llamada evite un segundo examen para obtener los datos en un í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;
}

La combinación de SequencePosition y TryGet actúa como enumerador. El campo de posición se modifica al principio de cada iteración para que sea el inicio de cada segmento dentro de ReadOnlySequence<T>.

El método anterior existe como método de extensión en ReadOnlySequence<T>. PositionOf se puede usar para simplificar el código anterior:

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

Procesamiento de ReadOnlySequence<T>

El procesamiento de ReadOnlySequence<T> puede ser difícil, dado que los datos podrían estar divididos en varios segmentos dentro de la secuencia. Para obtener el mejor rendimiento, divida el código en dos rutas de acceso:

  • Una ruta de acceso rápida que se ocupa del caso de un único segmento.
  • Una ruta de acceso lenta que se ocupa de la división de los datos entre segmentos.

Existen varios enfoques que se pueden usar para procesar datos en secuencias de varios segmentos:

  • Use SequenceReader<T>.
  • Analizar el segmento de datos por segmento y mantener el seguimiento de SequencePosition y del índice dentro del segmento analizado. De esta forma, se evitan asignaciones innecesarias, aunque puede ser ineficaz, especialmente en el caso de búferes pequeños.
  • Copiar ReadOnlySequence<T> en una matriz contigua y tratarla como un solo búfer:
    • Si el tamaño de ReadOnlySequence<T> es pequeño, puede ser razonable copiar los datos en un búfer asignado por la pila mediante el operador stackalloc.
    • Copie ReadOnlySequence<T> en una matriz agrupada mediante ArrayPool<T>.Shared.
    • Mediante ReadOnlySequence<T>.ToArray(). Este método no se recomienda en las rutas de acceso activas, ya que asigna un nuevo elemento T[] en el montón.

En los siguientes ejemplos se muestran algunos casos comunes de procesamiento de ReadOnlySequence<byte>:

Procesamiento de datos binarios

En el siguiente ejemplo se analiza una longitud de entero bid endian de 4 bytes desde el inicio de 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;
}
Procesamiento de datos de texto

En el ejemplo siguiente:

  • Encuentra la primera línea nueva (\r\n) en ReadOnlySequence<byte> y la devuelve mediante el parámetro "line" de salida.
  • Recorta esa línea, sin incluir \r\n del búfer 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 vacíos

Es válido almacenar segmentos vacíos dentro de ReadOnlySequence<T>. Pueden aparecer segmentos vacíos mientras se enumeran los segmentos de manera explícita:

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

El código anterior crea una estructura ReadOnlySequence<byte> con segmentos vacíos y muestra cómo afectan estos a las distintas API:

  • ReadOnlySequence<T>.Slice con un valor SequencePosition que apunta a un segmento vacío conserva ese segmento.
  • ReadOnlySequence<T>.Slice con un valor entero omite los segmentos vacíos.
  • Al enumerar ReadOnlySequence<T>, se enumeran los segmentos vacíos.

Posibles problemas con ReadOnlySequence<T > y SequencePosition

Hay varios resultados inusuales cuando se trabaja con ReadOnlySequence<T>/SequencePosition frente a una representación ReadOnlySpan<T>normal /ReadOnlyMemory<T>/T[]/int:

  • SequencePosition es un marcador de posición para una estructura ReadOnlySequence<T> específica, no una posición absoluta. Dado que es relativo a una estructura ReadOnlySequence<T> específica, no tiene ningún significado si se usa fuera de la estructura ReadOnlySequence<T> donde se origina.
  • No se pueden realizar operaciones aritméticas en SequencePosition sin ReadOnlySequence<T>. Eso significa que hacer cosas básicas, como position++, se escribe position = ReadOnlySequence<T>.GetPosition(1, position).
  • GetPosition(long)no admite índices negativos. Esto significa que es imposible obtener del segundo al último carácter sin recorrer todos los segmentos.
  • No se pueden comparar dos valores SequencePosition, lo que dificulta:
    • Saber si una posición es mayor o menor que otra.
    • Escribir algunos algoritmos de análisis.
  • ReadOnlySequence<T> es mayor que una referencia de objeto y debe pasarse mediante in o ref siempre que sea posible. Pasar ReadOnlySequence<T> mediante in o ref reduce las copias de la estructura.
  • Segmentos vacíos:
    • Son válidos dentro de ReadOnlySequence<T>.
    • Pueden aparecer cuando se recorre en iteración mediante el método ReadOnlySequence<T>.TryGet.
    • Pueden aparecer segmentando la secuencia mediante el método ReadOnlySequence<T>.Slice() con objetos SequencePosition.

SequenceReader<T>

SequenceReader<T>:

  • Es un nuevo tipo que se presentó en .NET Core 3.0 para simplificar el procesamiento de ReadOnlySequence<T>.
  • Unifica las diferencias entre un solo segmento ReadOnlySequence<T> y varios segmentos ReadOnlySequence<T>.
  • Proporciona asistentes para leer datos binarios y de texto (byte y char) que podrían estar o no divididos entre segmentos.

Existen métodos integrados que se ocupan del procesamiento de datos binarios y delimitados. En la siguiente sección se muestra el aspecto de esos mismos métodos con SequenceReader<T>:

Acceder a datos

SequenceReader<T> tiene métodos para enumerar datos dentro de ReadOnlySequence<T> directamente. El código siguiente es un ejemplo de procesamiento de ReadOnlySequence<byte> un byte cada vez:

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

CurrentSpan expone el valor Span del segmento actual, que es similar a lo que se hizo en el método manualmente.

Uso de la posición

El código siguiente es un ejemplo de implementación de FindIndexOf mediante 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;
}

Procesamiento de datos binarios

En el siguiente ejemplo se analiza una longitud de entero bid endian de 4 bytes desde el inicio de ReadOnlySequence<byte>.

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

Procesamiento de datos 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 comunes de SequenceReader<T>

  • Dado que SequenceReader<T> es una estructura mutable, siempre debe pasarse mediante una referencia.
  • SequenceReader<T> es una estructura de referencia, de modo que solo se puede usar en métodos sincrónicos y no se puede almacenar en campos. Para más información, consulte Cómo evitar asignaciones.
  • SequenceReader<T> está optimizado para su uso como lector de solo avance. Rewind está destinado a copias de seguridad pequeñas que no se pueden abordar mediante otras API de Read, Peek y IsNext.