Работа с буферами в .NETWork 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:

  • Запрашивает буфер длиной не менее 5 байт у IBufferWriter<byte> с помощью GetSpan(5).Requests a buffer of at least 5 bytes from the IBufferWriter<byte> using GetSpan(5).
  • Записывает байты для строки Hello (ASCII) при получении Span<byte>.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.

Этот метод записи использует буфер Memory<T>/Span<T>, предоставленный IBufferWriter<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.

Распространенные проблемы с IBufferWriterIBufferWriter 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.

Третье представление является наиболее интересным, так как оно влияет на производительность различных операций с 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.
  • Состоит из двух частей: целого числа и объекта.Consists of two parts, an integer and an object. Представление этих двух значений связано с реализацией 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 вызывающий может пропустить вторую проверку для получения данных в определенном индексе.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;
}

Сочетание SequencePosition и TryGet выполняет функции перечислителя.The combination of SequencePosition and TryGet acts like an enumerator. Поле позиции изменяется в начале каждой итерации для того, чтобы оно находилось в начале каждого сегмента в 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. Для лучшей производительности разделите код на две задачи: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> в смежный массив и работайте с ним как с отдельным буфером.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.
    • Скопируйте ReadOnlySequence<T> в массив в пуле с помощью ArrayPool<T>.Shared.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

В следующем примере обрабатывается целое число (длиной 4 байта) с обратным порядком байтов с начала ReadOnlySequence<byte>.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:

  • Выполняется поиск первого символа новой строки (\r\n) в ReadOnlySequence<byte>, которое возвращается через выходной параметр line.Finds the first newline (\r\n) in the ReadOnlySequence<byte> and returns it via the out 'line' parameter.
  • Затем эта строка обрезается, исключая \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:

  • Сочетание ReadOnlySequence<T>.Slice со структурой SequencePosition, указывающей на пустой сегмент, сохраняет такой сегмент.ReadOnlySequence<T>.Slice with a SequencePosition pointing to an empty segment preserves that segment.
  • ReadOnlySequence<T>.Slice с int позволяет пропустить пустой сегмент.ReadOnlySequence<T>.Slice with an int skips over the empty segments.
  • При перечислении ReadOnlySequence<T> перечисляются пустые сегменты.Enumerating the ReadOnlySequence<T> enumerates the empty segments.

Возможные проблемы с ReadOnlySequence<T> и SequencePositionPotential 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.
  • Арифметические операции с SequencePosition нельзя выполнять без ReadOnlySequence<T>.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. Таким образом, чтобы получить предпоследний символ, необходимо пройти все сегменты.That means it's impossible to get the second to last character without walking all segments.
  • Нельзя сравнить два объекта 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. Передача ReadOnlySequence<T> посредством in или ref позволяет сократить количество копирований структуры.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.
    • Могут появиться при разбиении последовательности с помощью метода ReadOnlySequence<T>.Slice() с объектами SequencePosition.Can appear slicing the sequence using the ReadOnlySequence<T>.Slice() method with SequencePosition objects.

SequenceReader<T>SequenceReader<T>

SequenceReader<T>.SequenceReader<T>:

  • Новый тип, который появился в .NET Core 3.0. Он позволяет упростить обработку ReadOnlySequence<T>.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>.
  • Предоставляет вспомогательные методы для чтения двоичных и текстовых данных (byte и char), которые можно разбивать на сегменты.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. В следующем коде приведен пример обработки ReadOnlySequence<byte> (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

В следующем коде приведен пример реализации FindIndexOf с использованием SequenceReader<T>: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

В следующем примере обрабатывается целое число (длиной 4 байта) с обратным порядком байтов с начала ReadOnlySequence<byte>.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;
}

Распространенные проблемы c SequenceReader<T>SequenceReader<T> common problems

  • SequenceReader<T> представляет собой изменяемую структуру, которую всегда нужно передавать с помощью ссылки.Because SequenceReader<T> is a mutable struct, it should always be passed by reference.
  • SequenceReader<T> — это ссылочная структура, которую можно использовать только в синхронных методах и нельзя хранить в полях.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 предназначается для небольших резервных копий, к которым нельзя обращаться с использованием других API Read, Peek и IsNext.Rewind is intended for small backups that can't be addressed utilizing other Read, Peek, and IsNext APIs.