Auf Englisch lesen

Freigeben über


Arbeiten mit Puffern in .NET

Dieser Artikel bietet eine Übersicht über die Typen, die das Lesen von Daten unterstützen, die über mehrere Puffer hinweg ausgeführt werden. Sie werden hauptsächlich zur Unterstützung von PipeReader-Objekten verwendet.

IBufferWriter<T>

System.Buffers.IBufferWriter<T> ist ein Vertrag für synchrone gepufferte Schreibvorgänge. Auf niedrigster Ebene gilt Folgendes für die Schnittstelle:

  • Sie ist einfach zu verwenden.
  • Sie bietet Zugriff auf Memory<T>- oder Span<T>-Elemente. Memory<T> und Span<T> erlauben Schreibvorgänge, und Sie können ermitteln, wie viele T-Elemente geschrieben wurden.
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);
}

Für die oben genannte Methode gilt:

  • Fordert einen Puffer mit mindestens 5 Bytes von IBufferWriter<byte> unter Verwendung von GetSpan(5) an.
  • Sie schreibt Bytes für die ASCII-Zeichenfolge „Hello“ in das zurückgegebene Span<byte>-Element.
  • Ruft IBufferWriter<T> auf, um anzugeben, wie viele Bytes in den Puffer geschrieben wurden.

Diese Methode zum Schreiben verwendet die Puffer Memory<T>/Span<T>, die von IBufferWriter<T> bereitgestellt werden. Alternativ dazu kann die Erweiterungsmethode Write verwendet werden, um einen vorhandenen Puffer in das IBufferWriter<T>-Element zu kopieren. Write ruft GetSpan/Advance je nach Bedarf auf, daher muss nach dem Schreiben Advance nicht aufgerufen werden:

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> ist eine Implementierung des IBufferWriter<T>-Elements, dessen Sicherungsspeicher ein einzelnes zusammenhängendes Array ist.

Häufige IBufferWriter-Probleme

  • GetSpan und GetMemory geben einen Puffer mit mindestens der angeforderten Menge an Arbeitsspeicher zurück. Gehen Sie nicht von genauen Puffergrößen aus.
  • Es gibt keine Garantie, dass aufeinanderfolgende Aufrufe denselben Puffer oder dieselbe Puffergröße zurückgeben.
  • Nach dem Aufrufen von Advance muss ein neuer Puffer angefordert werden, um das Schreiben weiterer Daten fortzusetzen. In einen zuvor abgerufenen Puffer kann erst nach dem Aufruf von Advance geschrieben werden.

ReadOnlySequence<T>

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

ReadOnlySequence<T> ist eine Struktur, die eine zusammenhängende oder eine nicht zusammenhängende Sequenz von T darstellen kann. Sie kann aus Folgendem erstellt werden:

  1. Eine T[]
  2. Eine ReadOnlyMemory<T>
  3. Ein Paar aus einem verknüpften Listenknoten ReadOnlySequenceSegment<T> und einem Index zum Darstellen der Start- und Endposition der Sequenz.

Die dritte Darstellung ist die interessanteste, da sie sich auf die Leistung verschiedener Vorgänge in ReadOnlySequence<T> auswirkt:

Darstellung Vorgang Komplexität
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)

Aufgrund dieser gemischten Darstellung macht ReadOnlySequence<T> Indizes als SequencePosition anstelle einer Ganzzahl verfügbar. Für eine SequencePosition gilt:

  • Es handelt sich um einen nicht transparenten Wert, der einen Index für das ReadOnlySequence<T>-Element darstellt, aus dem der Wert stammt.
  • Sie besteht aus zwei Teilen: einer Ganzzahl und einem Objekt. Die Darstellung dieser beiden Werte ist an die Implementierung von ReadOnlySequence<T> gebunden.

Zugriff auf Daten

ReadOnlySequence<T> macht Daten als aufzählbares Element von ReadOnlyMemory<T> verfügbar. Die Aufzählung jedes der Segmente kann mithilfe eines einfachen foreach-Vorgangs durchgeführt werden:

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

Die oben genannte Methode durchsucht jedes Segment nach einem bestimmten Byte. Wenn Sie die SequencePosition jedes Segments nachverfolgen müssen, ist ReadOnlySequence<T>.TryGet besser geeignet. Das nächste Beispiel ändert den oben stehenden Code so, dass anstelle einer Ganzzahl eine SequencePosition zurückgegeben wird. Die Rückgabe einer SequencePosition bietet den Vorteil, dass die aufrufende Funktion keine zweite Überprüfung durchführen muss, um die Daten an einer bestimmten Indexposition abzurufen.

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

Die Kombination aus SequencePosition und TryGet fungiert wie ein Enumerator. Das Positionsfeld wird beim Start jeder Iteration so geändert, dass es als Start jedes Segments innerhalb des ReadOnlySequence<T>-Elements dient.

Die oben beschriebene Methode ist als Erweiterungsmethode in ReadOnlySequence<T> vorhanden. PositionOf kann zur Vereinfachung des vorherigen Codes verwendet werden:

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

Verarbeiten eines ReadOnlySequence<T>-Elements

Die Verarbeitung eines ReadOnlySequence<T>-Elements kann schwierig sein, weil Daten möglicherweise auf mehrere Segmente innerhalb der Sequenz aufgeteilt sind. Um die bestmögliche Leistung zu erzielen, teilen Sie den Code in zwei Pfade auf:

  • Ein schneller Pfad, der den Fall eines einzelnen Segments verarbeitet.
  • Ein langsamer Pfad, der auf Segmente aufgeteilte Daten verarbeitet.

Zur Verarbeitung von Daten in Sequenzen mit mehreren Segmenten können verschiedene Ansätze verwendet werden:

  • Verwenden Sie SequenceReader<T>.
  • Analysieren Sie Daten segmentweise, und verfolgen Sie die SequencePosition und den Index innerhalb des analysierten Segments nach. Dieses Vorgehen vermeidet unnötige Zuteilungen, ist aber insbesondere für kleine Puffer möglicherweise ineffizient.
  • Kopieren Sie das ReadOnlySequence<T>-Element in ein zusammenhängendes Array, und behandeln Sie es wie einen einzelnen Puffer:
    • Wenn das ReadOnlySequence<T>-Element klein ist, kann es sinnvoll sein, die Daten mithilfe des stackalloc-Operators in einen Puffer mit Stapelzuordnung zu kopieren.
    • Kopieren Sie das ReadOnlySequence<T>-Element mithilfe von ArrayPool<T>.Shared in ein in einem Pool zusammengefasstes Array.
    • Verwenden Sie ReadOnlySequence<T>.ToArray(). Dies ist im langsamsten Pfad nicht empfehlenswert, da dadurch ein neues T[]-Element im Heap zugewiesen wird.

Die folgenden Beispiele veranschaulichen einige gängige Fälle für die Verarbeitung von ReadOnlySequence<byte>:

Verarbeiten von Binärdaten

Das folgende Beispiel analysiert eine 4 Byte lange Big-Endian-Ganzzahl ab dem Start des ReadOnlySequence<byte>-Elements.

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;
}
Verarbeiten von Textdaten

Im Beispiel unten geschieht Folgendes:

  • Die erste neue Zeile (\r\n) in ReadOnlySequence<byte> wird gefunden und über den Ausgabeparameter „line“ zurückgegeben.
  • Diese Zeile wird abgeschnitten, dabei wird die Zeichenfolge \r\n aus dem Eingabepuffer ausgeschlossen.
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;
}
Leere Segmente

Es ist möglich, leere Segmente in einem ReadOnlySequence<T>-Element zu speichern. Leere Segmente können beim expliziten Aufzählen von Segmenten auftreten:

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

Der oben stehende Code erstellt ein ReadOnlySequence<byte>-Element mit leeren Segmenten und zeigt, wie sich diese leeren Segmente auf die verschiedenen APIs auswirken:

  • ReadOnlySequence<T>.Slice mit einer SequencePosition, die auf ein leeres Segment zeigt, behält dieses Segment bei.
  • ReadOnlySequence<T>.Slice mit einem int-Wert überspringt die leeren Segmente.
  • Beim Aufzählen des ReadOnlySequence<T>-Elements werden die leeren Segmente aufgezählt.

Potenzielle Probleme mit ReadOnlySequence<T> und SequencePosition

Beim Verarbeiten von ReadOnlySequence<T>/SequencePosition gibt es im Vergleich zu einer normalen ReadOnlySpan<T>/ReadOnlyMemory<T>/T[]/int-Verarbeitung verschiedene ungewöhnliche Ergebnisse:

  • SequencePosition ist eine Positionsmarkierung für ein bestimmtes ReadOnlySequence<T>-Element, keine absolute Position. Da diese Markierung relativ zu einem bestimmten ReadOnlySequence<T>-Element ist, hat sie außerhalb des ReadOnlySequence<T>-Elements, aus dem sie stammt, keine Bedeutung.
  • In der SequencePosition können ohne das ReadOnlySequence<T>-Element keine arithmetischen Berechnungen ausgeführt werden. Das bedeutet, dass einfache Vorgänge wie position++ als position = ReadOnlySequence<T>.GetPosition(1, position) geschrieben werden.
  • GetPosition(long) unterstützt keine negativen Indizes. Das bedeutet, dass es unmöglich ist, das vorletzte Zeichen abzurufen, ohne alle Segmente zu durchlaufen.
  • Zwei SequencePosition-Elemente können nicht miteinander verglichen werden, sodass folgende Vorgänge schwierig sind:
    • Ermitteln, ob eine Position größer oder kleiner ist als eine andere Position.
    • Schreiben einiger Analysealgorithmen.
  • ReadOnlySequence<T> ist größer als ein Objektverweis und sollte nach Möglichkeit durch in oder ref übergeben werden. Durch Übergeben von ReadOnlySequence<T> durch in oder ref werden weniger Kopien der Struktur erstellt.
  • Für leere Segmente gilt:
    • Sie sind innerhalb eines ReadOnlySequence<T>-Elements gültig.
    • Sie können beim Durchlaufen mithilfe der ReadOnlySequence<T>.TryGet-Methode angezeigt werden.
    • Sie können beim Aufteilen der Sequenz in Slices mithilfe der ReadOnlySequence<T>.Slice()-Methode mit SequencePosition-Objekten angezeigt werden.

SequenceReader<T>

SequenceReader<T>:

  • Es handelt sich um einen neuen Typ, der in .NET Core 3.0 eingeführt wurde, um die Verarbeitung eines ReadOnlySequence<T>-Elements zu vereinfachen.
  • Durch den Typ entfallen die Unterschiede zwischen einem ReadOnlySequence<T>-Element mit einem Segment und einem ReadOnlySequence<T>-Element mit mehreren Segmenten.
  • Der Typ stellt Hilfsfunktionen für das Lesen von Binär- und Textdaten bereit (byte und char), die möglicherweise auf mehrere Segmente aufgeteilt sind.

Es gibt integrierte Methoden für die Verarbeitung sowohl von binären Daten als auch von Daten mit Trennzeichen. Der folgende Abschnitt veranschaulicht, wie diese Methoden mit dem SequenceReader<T>-Element aussehen:

Zugriff auf Daten

SequenceReader<T> verfügt über Methoden zum Aufzählen von Daten direkt im ReadOnlySequence<T>-Element. Der folgende Code ist ein Beispiel für die Verarbeitung jeweils eines ReadOnlySequence<byte>-Elements pro byte:

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

CurrentSpan macht das Span-Element des aktuellen Segments verfügbar – dies entspricht der manuellen Ausführung des Vorgangs in der Methode.

Verwenden der Position

Der folgende Code ist eine Beispielimplementierung von FindIndexOf unter Verwendung des SequenceReader<T>-Elements:

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

Verarbeiten von Binärdaten

Das folgende Beispiel analysiert eine 4 Byte lange Big-Endian-Ganzzahl ab dem Start des ReadOnlySequence<byte>-Elements.

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

Verarbeiten von Textdaten

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

Häufige SequenceReader<T>-Probleme

  • Da SequenceReader<T> eine änderbare Struktur ist, sollte das Element immer durch Verweis übergeben werden.
  • SequenceReader<T> ist eine ref-Struktur und kann daher nur in synchronen Methoden verwendet und nicht in Feldern gespeichert werden. Weitere Informationen finden Sie unter Vermeiden von Reservierungen.
  • SequenceReader<T> ist für die Verwendung als Vorwärtslesefunktion optimiert. Rewind ist für Sicherungen mit geringem Umfang vorgesehen, die nicht über andere Read-, Peek- oder IsNext-APIs verarbeitet werden können.