.NET でのバッファーの使用Work with Buffers in .NET

この記事では、複数のバッファーで実行されるデータの読み取りに役立つ型の概要について説明します。This article provides an overview of types that help read data that runs across multiple buffers. これらは主に、PipeReader オブジェクトをサポートするために使用されます。They're primarily used to support PipeReader objects.

IBufferWriter<T>IBufferWriter<T>

System.Buffers.IBufferWriter<T> は、バッファーの同期を行う書き込みのためのコントラクトです。System.Buffers.IBufferWriter<T> is a contract for synchronous buffered writing. 最下位レベルでは、インターフェイスは次のようになります。At the lowest level, the interface:

  • 基本的で、簡単に使用できます。Is basic and not difficult to use.
  • Memory<T> または Span<T> へのアクセスが可能です。Allows access to a Memory<T> or Span<T>. Memory<T> または Span<T> に書き込むことができ、書き込まれた T 項目の数を確認できます。The Memory<T> or Span<T> can be written to and you can determine how many T items were written.
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);
}

上記のメソッドでは:The preceding method:

  • GetSpan(5) を使用して、IBufferWriter<byte> から少なくとも 5 バイトのバッファーを要求します。Requests a buffer of at least 5 bytes from the IBufferWriter<byte> using GetSpan(5).
  • 返された Span<byte> に、ASCII 文字列 "Hello" のバイトを書き込みます。Writes bytes for the ASCII string "Hello" to the returned Span<byte>.
  • IBufferWriter<T> を呼び出して、バッファーに書き込まれたバイト数を示します。Calls IBufferWriter<T> to indicate how many bytes were written to the buffer.

この書き込みメソッドでは、IBufferWriter<T> によって提供される Memory<T>/Span<T> バッファーが使用されています。This method of writing uses the Memory<T>/Span<T> buffer provided by the IBufferWriter<T>. 代わりに、Write 拡張メソッドを使用して既存のバッファーを IBufferWriter<T> にコピーすることもできます。Alternatively, the Write extension method can be used to copy an existing buffer to the IBufferWriter<T>. Write によって、GetSpan/Advance を呼び出す処理が適切に実行されます。そのため、書き込み後に Advance を呼び出す必要はありません。Write does the work of calling GetSpan/Advance as appropriate, so there's no need to call Advance after writing:

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> の実装です。ArrayBufferWriter<T> is an implementation of IBufferWriter<T> whose backing store is a single contiguous array.

IBufferWriter の一般的な問題IBufferWriter common problems

  • GetSpan および GetMemory では、少なくとも要求された量のメモリを持つバッファーを返します。GetSpan and GetMemory return a buffer with at least the requested amount of memory. 厳密なバッファー サイズを想定しないでください。Don't assume exact buffer sizes.
  • 連続する呼び出しで同じバッファーまたは同じサイズのバッファーが返される保証はありません。There's no guarantee that successive calls will return the same buffer or the same-sized buffer.
  • さらにデータの書き込みを続行するには、Advance を呼び出した後に新しいバッファーを要求する必要があります。A new buffer must be requested after calling Advance to continue writing more data. Advance を呼び出した後に、前に取得したバッファーに書き込むことはできません。A previously acquired buffer cannot be written to after Advance has been called.

ReadOnlySequence<T>ReadOnlySequence<T>

パイプ内のメモリと、その下に読み取り専用メモリのシーケンス位置を示す ReadOnlySequence

ReadOnlySequence<T> は、T の隣接したシーケンス、または隣接しないシーケンスを表すことができる構造体です。ReadOnlySequence<T> is a struct that can represent a contiguous or noncontiguous sequence of T. これは次のものから構築できます。It can be constructed from:

  1. T[]A T[]
  2. ReadOnlyMemory<T>A ReadOnlyMemory<T>
  3. リンク リスト ノード ReadOnlySequenceSegment<T> と、シーケンスの開始位置および終了位置を表すインデックスのペア。A pair of linked list node ReadOnlySequenceSegment<T> and index to represent the start and end position of the sequence.

最も興味深いのは 3 番目の表現方法です。ReadOnlySequence<T> のさまざまな操作に対してパフォーマンスへの影響があるためです。The third representation is the most interesting one as it has performance implications on various operations on the ReadOnlySequence<T>:

表現Representation 操作Operation 複雑さComplexity
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(SequencePostion, SequencePostion) 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(SequencePostion, SequencePostion) O(1)

この混合表現のため、ReadOnlySequence<T> では整数ではなく SequencePosition としてインデックスが公開されます。Because of this mixed representation, the ReadOnlySequence<T> exposes indexes as SequencePosition instead of an integer. SequencePosition は:A SequencePosition:

  • 発生元の ReadOnlySequence<T> のインデックスを表す非透過的な値です。Is an opaque value that represents an index into the ReadOnlySequence<T> where it originated.
  • 整数とオブジェクトの 2 つの部分で構成されます。Consists of two parts, an integer and an object. これらの 2 つの値によって表されるものは、ReadOnlySequence<T> の実装に関連付けられています。What these two values represent are tied to the implementation of ReadOnlySequence<T>.

データにアクセスするAccess data

ReadOnlySequence<T> では、列挙可能な ReadOnlyMemory<T> としてデータが公開されます。The ReadOnlySequence<T> exposes data as an enumerable of ReadOnlyMemory<T>. 基本的な foreach を使用して、各セグメントの列挙を行うことができます。Enumerating each of the segments can be done using a basic 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;
}

上記のメソッドでは、各セグメントで特定のバイトを検索しています。The preceding method searches each segment for a specific byte. 各セグメントの SequencePosition を追跡する必要がある場合は、ReadOnlySequence<T>.TryGet の方が適しています。If you need to keep track of each segment's SequencePosition, ReadOnlySequence<T>.TryGet is more appropriate. 次のサンプルでは、整数ではなく SequencePosition を返すように前のコードを変更しています。The next sample changes the preceding code to return a SequencePosition instead of an integer. SequencePosition を返すことにより、呼び出し元が特定のインデックスのデータを取得するための 2 回目のスキャンを回避できるという利点があります。Returning a SequencePosition has the benefit of allowing the caller to avoid a second scan to get the data at a specific index.

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

    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(position, index);
        }
    }
    return null;
}

SequencePositionTryGet の組み合わせは列挙子のように動作します。The combination of SequencePosition and TryGet acts like an enumerator. position フィールドは、各反復の開始時に、ReadOnlySequence<T> 内の各セグメントの開始位置に変更されます。The position field is modified at the start of each iteration to be start of each segment within the ReadOnlySequence<T>.

上記のメソッドは、ReadOnlySequence<T> の拡張メソッドとして存在しています。The preceding method exists as an extension method on ReadOnlySequence<T>. PositionOf を使用すると、上記のコードを簡略化できます。PositionOf can be used to simplify the preceding code:

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

ReadOnlySequence<T> の処理Process a ReadOnlySequence<T>

ReadOnlySequence<T> の処理は困難な場合があります。シーケンス内の複数のセグメントにまたがってデータが分割されている可能性があるためです。Processing a ReadOnlySequence<T> can be challenging since data may be split across multiple segments within the sequence. 最適なパフォーマンスを得るには、コードを次の 2 つのパスに分割します。For the best performance, split code into two paths:

  • 単一セグメントのケースを処理する高速パス。A fast path that deals with the single segment case.
  • セグメント間に分割されたデータを処理する低速パス。A slow path that deals with the data split across segments.

複数のセグメントに分割されたシーケンスのデータを処理するには、いくつかの方法があります。There are a few approaches that can be used to process data in multi-segmented sequences:

  • SequenceReader<T> を使用する。Use the SequenceReader<T>.
  • セグメントごとにデータを解析し、解析されたセグメント内の SequencePosition とインデックスを追跡する。Parse data segment by segment, keeping track of the SequencePosition and index within the segment parsed. これにより不要な割り当てを回避できますが、非効率的になる可能性があります (特に小さなバッファーの場合)。This avoids unnecessary allocations but may be inefficient, especially for small buffers.
  • ReadOnlySequence<T> を隣接した配列にコピーし、それを 1 つのバッファーとして扱う。Copy the ReadOnlySequence<T> to a contiguous array and treat it like a single buffer:
    • ReadOnlySequence<T> のサイズが小さい場合は、stackalloc 演算子を使用して、スタック割り当てバッファーにデータをコピーすることが適切な場合があります。If the size of the ReadOnlySequence<T> is small, it may be reasonable to copy the data into a stack-allocated buffer using the stackalloc operator.
    • ArrayPool<T>.Shared を使用して、プールされた配列に ReadOnlySequence<T> をコピーします。Copy the ReadOnlySequence<T> into a pooled array using ArrayPool<T>.Shared.
    • ReadOnlySequence<T>.ToArray() を使用します。Use ReadOnlySequence<T>.ToArray(). これは、新しい T[] をヒープに割り当てるため、ホット パスでは推奨されません。This isn't recommended in hot paths as it allocates a new T[] on the heap.

次の例では、ReadOnlySequence<byte> を処理する一般的なケースをいくつか示しています。The following examples demonstrate some common cases for processing ReadOnlySequence<byte>:

バイナリ データの処理Process binary data

次の例では、ReadOnlySequence<byte> の先頭から、4 バイトのビッグ エンディアンの整数長を解析しています。The following example parses a 4-byte big-endian integer length from the start of the ReadOnlySequence<byte>.

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;
}
テキスト データの処理Process text data

次のような例です。The following example:

  • ReadOnlySequence<byte> 内の最初の改行 (\r\n) を検索し、out 'line' パラメーターを使用してそれを返します。Finds the first newline (\r\n) in the ReadOnlySequence<byte> and returns it via the out 'line' parameter.
  • その line をトリミングし、入力バッファーから \r\n を除外します。Trims that line, excluding the \r\n from the input buffer.
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;
}
空のセグメントEmpty segments

ReadOnlySequence<T> 内に空のセグメントを格納することができます。It's valid to store empty segments inside of a ReadOnlySequence<T>. セグメントを明示的に列挙するときに、空のセグメントが発生する可能性があります。Empty segments may occur while enumerating segments explicitly:

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 にどのような影響を与えるかを示しています。The preceding code creates a ReadOnlySequence<byte> with empty segments and shows how those empty segments affect the various APIs:

  • 空のセグメントを指す SequencePosition を使用した ReadOnlySequence<T>.Slice では、そのセグメントが保持されます。ReadOnlySequence<T>.Slice with a SequencePosition pointing to an empty segment preserves that segment.
  • int を使用した ReadOnlySequence<T>.Slice では、空のセグメントがスキップされます。ReadOnlySequence<T>.Slice with an int skips over the empty segments.
  • ReadOnlySequence<T> を列挙すると、空のセグメントが列挙されます。Enumerating the ReadOnlySequence<T> enumerates the empty segments.

ReadOnlySequence<T> と SequencePosition に関する潜在的な問題Potential problems with ReadOnlySequence<T> and SequencePosition

ReadOnlySequence<T>/SequencePosition を扱うときには、通常の ReadOnlySpan<T>/ReadOnlyMemory<T>/T[]/int に対して、いくつかの通常とは異なる結果が発生します。There are several unusual outcomes when dealing with a ReadOnlySequence<T>/SequencePosition vs. a normal ReadOnlySpan<T>/ReadOnlyMemory<T>/T[]/int:

  • SequencePosition は特定の ReadOnlySequence<T> の位置マーカーであり、絶対位置ではありません。SequencePosition is a position marker for a specific ReadOnlySequence<T>, not an absolute position. これは特定の ReadOnlySequence<T> を基準にするため、発生元の ReadOnlySequence<T> の外部で使用されても意味がありません。Because it's relative to a specific ReadOnlySequence<T>, it doesn't have meaning if used outside of the ReadOnlySequence<T> where it originated.
  • ReadOnlySequence<T> を使用せずに SequencePosition に対する算術演算を行うことはできません。Arithmetic can't be performed on SequencePosition without the ReadOnlySequence<T>. つまり、position++ などの基本的な処理は、ReadOnlySequence<T>.GetPosition(position, 1) のように記述されます。That means doing basic things like position++ is written ReadOnlySequence<T>.GetPosition(position, 1).
  • GetPosition(long) では、負のインデックスがサポートされていませんGetPosition(long) does not support negative indexes. つまり、すべてのセグメントをたどることなく最後から 2 番目の文字を取得することはできません。That means it's impossible to get the second to last character without walking all segments.
  • 2 つの SequencePosition を比較できないため、次のことが困難になります。Two SequencePosition can't be compared, making it difficult to:
    • ある位置が別の位置より大きいか小さいかを確認する。Know if one position is greater than or less than another position.
    • いくつかの解析アルゴリズムを記述する。Write some parsing algorithms.
  • ReadOnlySequence<T> はオブジェクト参照よりも大きいため、可能な場合は in または ref によって渡す必要があります。ReadOnlySequence<T> is bigger than an object reference and should be passed by in or ref where possible. in または ref によって ReadOnlySequence<T> を渡すことで、struct のコピーを減らすことができます。Passing ReadOnlySequence<T> by in or ref reduces copies of the struct.
  • 空のセグメントは:Empty segments:
    • ReadOnlySequence<T> 内で有効です。Are valid within a ReadOnlySequence<T>.
    • ReadOnlySequence<T>.TryGet メソッドを使った反復処理中に発生する可能性があります。Can appear when iterating using the ReadOnlySequence<T>.TryGet method.
    • SequencePosition オブジェクトと共に ReadOnlySequence<T>.Slice() メソッドを使ったシーケンスのスライスで発生する可能性があります。Can appear slicing the sequence using the ReadOnlySequence<T>.Slice() method with SequencePosition objects.

SequenceReader<T>SequenceReader<T>

SequenceReader<T>:SequenceReader<T>:

  • ReadOnlySequence<T> の処理を簡略化するために .NET Core 3.0 で導入された新しい型です。Is a new type that was introduced in .NET Core 3.0 to simplify the processing of a ReadOnlySequence<T>.
  • 単一セグメントの ReadOnlySequence<T> と複数セグメントの ReadOnlySequence<T> の違いが統合されます。Unifies the differences between a single segment ReadOnlySequence<T> and multi-segment ReadOnlySequence<T>.
  • 複数のセグメントに分割されている場合でもされていない場合でも、バイナリ データとテキスト データ (bytechar) を読み取るためのヘルパーが提供されます。Provides helpers for reading binary and text data (byte and char) that may or may not be split across segments.

バイナリ データと区切られたデータの両方を処理するための組み込みメソッドが用意されています。There are built-in methods for dealing with processing both binary and delimited data. 以下のセクションでは、これらの同じメソッドが SequenceReader<T> でどのように使用されるかを示します。The following section demonstrates what those same methods look like with the SequenceReader<T>:

データにアクセスするAccess data

SequenceReader<T> には、ReadOnlySequence<T> 内のデータを直接列挙するためのメソッドが用意されています。SequenceReader<T> has methods for enumerating data inside of the ReadOnlySequence<T> directly. 次のコードは、一度に byte ずつ ReadOnlySequence<byte> を処理する例です。The following code is an example of processing a ReadOnlySequence<byte> a byte at a time:

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

CurrentSpan を使用すると、現在のセグメントの Span が公開されます。これは、このメソッド内で手動で行われたことに似ています。The CurrentSpan exposes the current segment's Span, which is similar to what was done in the method manually.

位置の使用Use position

次のコードでは、SequenceReader<T> を使った FindIndexOf の実装例を示します。The following code is an example implementation of FindIndexOf using the SequenceReader<T>:

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

バイナリ データの処理Process binary data

次の例では、ReadOnlySequence<byte> の先頭から、4 バイトのビッグ エンディアンの整数長を解析しています。The following example parses a 4-byte big-endian integer length from the start of the ReadOnlySequence<byte>.

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

テキスト データの処理Process text data

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

SequenceReader<T> の一般的な問題SequenceReader<T> common problems

  • SequenceReader<T> は変更可能な構造体であるため、常に参照渡しする必要があります。Because SequenceReader<T> is a mutable struct, it should always be passed by reference.
  • SequenceReader<T>ref struct であるため、同期メソッド内でのみ使用でき、フィールドに格納することはできません。SequenceReader<T> is a ref struct so it can only be used in synchronous methods and can't be stored in fields. 詳細については、「安全で効率的な C# コードを記述する」をご覧ください。For more information, see Write safe and efficient C# code.
  • SequenceReader<T> は、順方向専用のリーダーとして使用するために最適化されています。SequenceReader<T> is optimized for use as a forward-only reader. Rewind は、他の ReadPeekIsNext API を使用しても対処できない小規模なバックアップを目的としています。Rewind is intended for small backups that can't be addressed utilizing other Read, Peek, and IsNext APIs.