영어로 읽기

다음을 통해 공유


.NET에서 버퍼 작업

이 문서에서는 여러 버퍼에서 실행되는 데이터를 읽는 데 도움이 되는 형식을 개략적으로 설명합니다. 이러한 형식은 주로 PipeReader 개체를 지원하는 데 사용됩니다.

IBufferWriter<T>

System.Buffers.IBufferWriter<T>는 비동기 버퍼링 쓰기에 대한 계약입니다. 최저 수준에서 인터페이스는,

  • 기본적이며 사용하기 어렵지 않습니다.
  • Memory<T> 또는 Span<T>에 대한 액세스를 허용합니다. Memory<T> 또는 Span<T>에 쓸 수 있으며 작성된 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바이트의 버퍼를 요청합니다.
  • 반환된 Span<byte>에 ASCII 문자열 "Hello"를 위한 바이트를 씁니다.
  • 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>의 구현입니다.

IBufferWriter의 일반적인 문제

  • GetSpanGetMemory는 적어도 요청된 양의 메모리를 포함하는 버퍼를 반환합니다. 정확한 버퍼 크기를 가정하지 마세요.
  • 연속 호출이 동일한 버퍼 또는 동일한 크기의 버퍼를 반환한다는 보장은 없습니다.
  • 추가 데이터를 계속 작성하려면 Advance를 호출한 후 새 버퍼를 요청해야 합니다. Advance를 호출한 후에는 이전에 가져온 버퍼를 쓸 수 없습니다.

ReadOnlySequence<T>

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

ReadOnlySequence<T>T의 연속 또는 불연속 시퀀스를 나타낼 수 있는 구조체입니다. 다음에서 생성할 수 있습니다.

  1. T[]
  2. ReadOnlyMemory<T>
  3. 연결된 목록 노드 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;
}

SequencePositionTryGet의 조합은 열거자처럼 동작합니다. 위치 필드는 각 반복이 시작될 때 ReadOnlySequence<T> 내에서 각 세그먼트의 시작으로 수정됩니다.

위의 메서드는 ReadOnlySequence<T>에 대한 확장 메서드로 존재합니다. PositionOf는 위의 코드를 간소화하는 데 사용할 수 있습니다.

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

ReadOnlySequence<T> 처리

데이터가 시퀀스 내의 여러 세그먼트로 분할될 수 있으므로 ReadOnlySequence<T> 처리는 어려울 수 있습니다. 최상의 성능을 위해 코드를 다음 두 개의 경로로 분할합니다.

  • 단일 세그먼트 케이스를 처리하는 빠른 경로.
  • 여러 세그먼트로 분할된 데이터를 처리하는 느린 경로.

다중 분할된 시퀀스에서 데이터를 처리하는 데 사용할 수 있는 몇 가지 방법이 있습니다.

  • SequenceReader<T>를 사용합니다.
  • 구문 분석된 세그먼트 내에서 SequencePosition 및 인덱스를 추적하여 데이터를 세그먼트 단위로 구문 분석합니다. 이렇게 하면 불필요한 할당을 피할 수 있지만 특히 버퍼가 작은 경우에는 비효율적입니다.
  • ReadOnlySequence<T>를 연속된 배열에 복사하고 단일 버퍼처럼 처리합니다.
    • ReadOnlySequence<T> 크기가 작으면 stackalloc 연산자를 사용하여 스택 할당 버퍼에 데이터를 복사하는 것이 합리적입니다.
    • ArrayPool<T>.Shared를 사용하여 ReadOnlySequence<T>를 풀링된 배열에 복사합니다.
    • ReadOnlySequence<T>.ToArray()을 사용합니다. 이는 힙에서 새 T[]를 할당하므로 실행 부하 과다 경로에는 권장되지 않습니다.

다음 예제에서는 ReadOnlySequence<byte>를 처리하는 몇 가지 일반적인 사례를 보여 줍니다.

이진 데이터 처리

다음 예제에서는 ReadOnlySequence<byte>의 시작 부분에서 4바이트 Big Endian 정수 길이를 구문 분석합니다.

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는 해당 세그먼트를 유지합니다.
  • 정수가 포함된 ReadOnlySequence<T>.Slice는 빈 세그먼트를 건너뜁니다.
  • ReadOnlySequence<T>를 열거하면 빈 세그먼트가 열거됩니다.

ReadOnlySequence<T> 및 SequencePosition의 잠재적 문제점

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 또는 ref에 의해 전달되어야 합니다. in 또는 ref를 통해 ReadOnlySequence<T>를 전달하면 구조체 복사본이 줄어듭니다.
  • 빈 세그먼트:
    • ReadOnlySequence<T> 내에서 유효합니다.
    • ReadOnlySequence<T>.TryGet 메서드를 사용하여 반복할 때 표시될 수 있습니다.
    • ReadOnlySequence<T>.Slice() 메서드와 SequencePosition 개체를 사용하여 시퀀스를조각화할 수 있습니다.

SequenceReader<T>

SequenceReader<T>:

  • ReadOnlySequence<T> 처리를 간소화하기 위해 .NET Core 3.0에 도입된 새로운 형식입니다.
  • 단일 세그먼트 ReadOnlySequence<T>와 다중 세그먼트 ReadOnlySequence<T> 간의 차이를 통합합니다.
  • 세그먼트 간에 분할될 수 있거나 분할될 수 없는 이진 및 텍스트 데이터(bytechar)를 읽기 위한 도우미를 제공합니다.

이진 데이터와 구분된 데이터를 모두 처리하는 기본 제공 메서드가 있습니다. 다음 섹션에서는 이러한 메서드가 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바이트 Big Endian 정수 길이를 구문 분석합니다.

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

SequenceReader<T>의 일반적인 문제

  • SequenceReader<T>는 변경 가능한 구조체이므로 항상 reference로 전달되어야 합니다.
  • SequenceReader<T>ref struct이므로 동기 메서드에만 사용할 수 있고 필드에 저장할 수 없습니다. 자세한 내용은 할당 방지를 참조하세요.
  • SequenceReader<T>는 정방향 전용 판독기로 사용하도록 최적화되어 있습니다. Rewind는 다른 Read, PeekIsNext API를 활용하여 해결할 수 없는 작은 백업에 사용됩니다.