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.
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>
undSpan<T>
erlauben Schreibvorgänge, und Sie können ermitteln, wie vieleT
-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 vonGetSpan(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.
GetSpan
undGetMemory
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 vonAdvance
geschrieben werden.
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:
- Eine
T[]
- Eine
ReadOnlyMemory<T>
- 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.
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);
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 neuesT[]
-Element im Heap zugewiesen wird.
- Wenn das
Die folgenden Beispiele veranschaulichen einige gängige Fälle für die Verarbeitung von ReadOnlySequence<byte>
:
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;
}
Im Beispiel unten geschieht Folgendes:
- Die erste neue Zeile (
\r\n
) inReadOnlySequence<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;
}
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 einerSequencePosition
, 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.
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 bestimmtesReadOnlySequence<T>
-Element, keine absolute Position. Da diese Markierung relativ zu einem bestimmtenReadOnlySequence<T>
-Element ist, hat sie außerhalb desReadOnlySequence<T>
-Elements, aus dem sie stammt, keine Bedeutung.- In der
SequencePosition
können ohne dasReadOnlySequence<T>
-Element keine arithmetischen Berechnungen ausgeführt werden. Das bedeutet, dass einfache Vorgänge wieposition++
alsposition = 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 vonReadOnlySequence<T>
durchin
oderref
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 mitSequencePosition
-Objekten angezeigt werden.
- Sie sind innerhalb eines
- 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 einemReadOnlySequence<T>
-Element mit mehreren Segmenten. - Der Typ stellt Hilfsfunktionen für das Lesen von Binär- und Textdaten bereit (
byte
undchar
), 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:
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.
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;
}
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);
}
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;
}
- 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 andereRead
-,Peek
- oderIsNext
-APIs verarbeitet werden können.
Feedback zu .NET
.NET ist ein Open Source-Projekt. Wählen Sie einen Link aus, um Feedback zu geben: