訓練
在 .NET 中使用緩衝區
本文提供一些類型的概觀,其可協助讀取跨多個緩衝區執行的資料。 類型主要用於支援 PipeReader 物件。
System.Buffers.IBufferWriter<T> 是同步緩衝寫入的合約。 在最低層級,介面:
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);
}
上述方法:
- 使用
GetSpan(5)
從IBufferWriter<byte>
要求至少 5 個位元組的緩衝區。 - 將 ASCII 字串 "Hello" 的位元組寫入至傳回的
Span<byte>
。 - 呼叫 IBufferWriter<T> 以指出寫入緩衝區的位元組數目。
這個寫入方法會使用 IBufferWriter<T>
所提供的 Memory<T>
/Span<T>
緩衝區。 或者,可以使用 Write 擴充方法將現有的緩衝區複製到 IBufferWriter<T>
。 Write
會適當地呼叫 GetSpan
/Advance
,因此在寫入之後不需要呼叫 Advance
:
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> 是 IBufferWriter<T>
的實作,其備份存放區是單一連續陣列。
GetSpan
和GetMemory
會傳回具有最低所要求記憶體數量的緩衝區。 請勿假設確切的緩衝區大小。- 無法保證後續呼叫會傳回相同的緩衝區或大小相同的緩衝區。
- 呼叫
Advance
繼續寫入更多資料之後,必須要求新的緩衝區。 呼叫Advance
之後,無法寫入先前取得的緩衝區。
ReadOnlySequence<T> 是結構,可表示 T
的連續或非連續序列。 它可以從下列項目建構:
- 進行
T[]
- 進行
ReadOnlyMemory<T>
- 一對連結的清單節點 ReadOnlySequenceSegment<T> 和索引,表示序列的開始和結束位置。
第三個表示法是最有趣的表示法,因為它對 ReadOnlySequence<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) |
由於這種混合表示法,所以 ReadOnlySequence<T>
會將索引公開為 SequencePosition
而不是整數。 SequencePosition
:
- 這是不透明值,表示其來源所在的
ReadOnlySequence<T>
索引。 - 包含兩個部分:整數和物件。 這兩個值代表的內容繫結至
ReadOnlySequence<T>
的實作。
ReadOnlySequence<T>
會將資料公開為可列舉的 ReadOnlyMemory<T>
。 您可以使用基本 foreach 來列舉每個區段:
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;
}
上述方法會搜尋每個區段是否有特定位元組。 如果您需要追蹤每個區段的 SequencePosition
,則 ReadOnlySequence<T>.TryGet 更為適合。 下一個範例會變更上述程式碼以傳回 SequencePosition
而不是整數。 傳回 SequencePosition
的優點是讓呼叫端能夠避免第二次掃描,以在特定索引上取得資料。
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;
}
SequencePosition
與 TryGet
組合的作用就像列舉程式一樣。 位置欄位會在每次反覆運算的開頭修改,以在 ReadOnlySequence<T>
內每個區段的開頭。
上述方法存在於 ReadOnlySequence<T>
上做為擴充方法。 PositionOf 可用來簡化上述程式碼:
SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data) => buffer.PositionOf(data);
處理 ReadOnlySequence<T>
可能會有挑戰性,因為資料可能會分割在序列內的多個區段。 為了達到最佳效能,請將程式碼分割成兩個路徑:
- 處理單一區段案例的快速路徑。
- 處理跨區段分割資料的緩慢路徑。
有幾個方法可用來處理多個分段序列中的資料:
- 使用
SequenceReader<T>
。 - 依區段剖析資料區段,追蹤所剖析區段內的
SequencePosition
和索引。 這可避免不必要的配置,但可能效率不佳,特別是小型緩衝區。 - 將
ReadOnlySequence<T>
複製到連續陣列,並將它視為單一緩衝區:- 如果
ReadOnlySequence<T>
的大小很小,使用 stackalloc 運算子將資料複製到堆疊配置的緩衝區可能很合理。 - 使用
ReadOnlySequence<T>
將 ArrayPool<T>.Shared 複製到集區式陣列。 - 使用
ReadOnlySequence<T>.ToArray()
。 這不建議在最忙碌路徑中執行,因為它會在堆積上配置新的T[]
。
- 如果
下列範例示範處理 ReadOnlySequence<byte>
的一些常見案例:
下列範例會從 ReadOnlySequence<byte>
開頭剖析 4 位元組的位元組由大到小整數長度。
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;
}
下列範例將:
- 尋找
ReadOnlySequence<byte>
中的第一個新行字元 (\r\n
),並透過 out 'line' 參數傳回它。 - 修剪該行,從輸入緩衝區排除
\r\n
。
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;
}
在 ReadOnlySequence<T>
內儲存空白區段是有效的。 明確列舉區段時,可能會出現空白區段:
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;
}
}
上述程式碼會建立具有空白區段的 ReadOnlySequence<byte>
,並示範這些空白區段如何影響各種 API:
SequencePosition
指向空白區段的ReadOnlySequence<T>.Slice
會保留該區段。- 具有 int 的
ReadOnlySequence<T>.Slice
會略過空白區段。 - 列舉
ReadOnlySequence<T>
會列舉空白區段。
處理 ReadOnlySequence<T>
/SequencePosition
與一般 ReadOnlySpan<T>
/ReadOnlyMemory<T>
/T[]
/int
時,有數個不尋常的結果:
SequencePosition
是特定ReadOnlySequence<T>
的位置標記,而不是絕對位置。 因為它相對於特定的ReadOnlySequence<T>
,所以如果在產生來源所在的ReadOnlySequence<T>
之外使用,它就沒有意義。- 若沒有
ReadOnlySequence<T>
,就無法在SequencePosition
上執行算術。 這表示position++
寫入position = ReadOnlySequence<T>.GetPosition(1, position)
之類的基本事項。 GetPosition(long)
不支援負索引。 這表示若沒有逐一查看所有區段,將無法取得第二個字元到最後一個字元。- 無法比較兩個
SequencePosition
,因此很難:- 瞭解某個位置是大於還是小於另一個位置。
- 編寫一些剖析演算法。
ReadOnlySequence<T>
大於物件參考,而且應該盡可能以 in 或傳址方式傳遞。 以in
或ref
方式傳遞ReadOnlySequence<T>
,會減少結構的複本。- 空白區段:
- 在
ReadOnlySequence<T>
內有效。 - 使用
ReadOnlySequence<T>.TryGet
方法進行反覆運算時,可能會顯示。 - 可以使用
ReadOnlySequence<T>.Slice()
方法搭配SequencePosition
物件來顯示切割序列。
- 在
- 這是在 .NET Core 3.0 中引進的新類型,可簡化
ReadOnlySequence<T>
的處理。 - 統一單一區段
ReadOnlySequence<T>
與多區段ReadOnlySequence<T>
之間的差異。 - 提供協助程式以讀取二進位和文字資料
byte
和char
),這些資料可能或可能不會跨區段分割。
有一些內建方法可處理二進位和分隔資料。 下一節示範使用 SequenceReader<T>
時,那些相同方法會呈現什麼:
SequenceReader<T>
具有直接列舉 ReadOnlySequence<T>
內資料的方法。 下列程式碼是一次處理 ReadOnlySequence<byte>
byte
的範例:
while (reader.TryRead(out byte b))
{
Process(b);
}
CurrentSpan
會公開目前區段的 Span
,這類似於手動在方法中完成的工作。
下列程式碼是使用 SequenceReader<T>
的 FindIndexOf
範例實作:
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;
}
下列範例會從 ReadOnlySequence<byte>
開頭剖析 4 位元組的位元組由大到小整數長度。
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;
}
其他資源
文件
-
了解如何在 .NET 中有效率地使用 I/O 管線,並避免程式碼發生問題。
-
使用 System.IO.Pipelines 的高效能 IO
System.IO.Pipelines 誕生於 .NET Core 小組所執行的工作,可讓您更輕鬆地在 .NET 中執行高效能 IO。在這個情節中,帕維爾·克里梅茨(@派克里姆)和大衛·福勒(@davidfowl)來到節目中,提供管線程序設計模型運作方式的概觀,並提供一些如何使用 API 的示範。[00:26] - System.IO.Pipelines 是什麼理由?[02:10] - 管道與數據流之間的效能比較[04:17] - 使用 Stream 的考慮[09:42] - 移至管道[13:45] - 用戶端伺服器示範[22:16] - 管道如何使用 C# 8 IAsyncEnumerable?[26:04] - 減少配置[28:46] - 開始使用管線實用連結NuGet 上的 System.IO.PipelinesSystem.IO.Pipelines:.NET 中的高效能 IOTCP 回應範例具有 ValueTask T> 和 ValueTask<的配置可用異步作業 Techempower Web Framework 基準檢驗.NET 設計檢閱:System.IO.Pi