.NET'te Arabelleklerle Çalışma

Bu makalede, birden çok arabellekte çalışan verilerin okunmasında yardımcı olan türlere genel bir bakış verilir. Öncelikle PipeReader nesnelerini desteklemek için kullanılırlar.

IBufferWriter<T>

System.Buffers.IBufferWriter<T> eşzamanlı arabellekli yazma için yapılan bir sözleşmedir. En düşük düzeyde, arabirim:

  • Temeldir ve kullanımı zor değildir.
  • Memory<T> veya Span<T> öğesine erişime izin verir. Memory<T> veya Span<T> öğesine yazılabilir ve kaç T öğenin yazıldığını belirleyebilirsiniz.
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);
}

Yukarıdaki yöntem:

  • IBufferWriter<byte> kullanarak GetSpan(5)'den en az 5 baytlık bir arabellek istemektedir.
  • ASCII karakter dizisi "Hello" için baytları döndürülen Span<byte> öğesine yazar.
  • Arabelleğe kaç bayt yazıldığını belirtmek için IBufferWriter<T> çağrıları yapılır.

Memory<T> / Span<T> tamponu tarafından sağlanan bu yazma yöntemini kullanırIBufferWriter<T>. Alternatif olarak, Write uzantı yöntemi mevcut bir arabelleği IBufferWriter<T> öğesine kopyalamak için kullanılabilir. Write arama GetSpan/Advance işini uygun şekilde yapar, bu nedenle yazdıktan sonra arama Advance yapmanıza gerek yoktur:

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>, yedekleme deposu tek bir bitişik dizi olan IBufferWriter<T>'in bir uygulamasıdır.

IBufferWriter yaygın sorunları

  • GetSpan ve GetMemory en az istenen bellek miktarına sahip bir arabellek döndürür. Kesin arabellek boyutlarını varsaymayın.
  • Ardışık çağrıların aynı arabelleği veya aynı boyutlu arabelleği döndüreceğinin garantisi yoktur.
  • Daha fazla veri yazmaya devam etmek için Advance çağrıldıktan sonra yeni bir arabellek istenmelidir. Bir işlev Advance çağrıldıktan sonra daha önce edinilen bir arabelleğe yazılamaz.

ReadOnlySequence<T>

ReadOnlySequence, pipe mekanizmasında belleği ve bu sıralı konumun altında salt okunur belleği gösteriyor

ReadOnlySequence<T> bir yapıdır ve T'ın bitişik veya bitişik olmayan bir dizisini temsil edebilir. Şu kaynaklardan oluşturulabilir:

  1. Bir T[]
  2. Bir ReadOnlyMemory<T>
  3. Dizinin başlangıç ve bitiş konumunu temsil eden bağlantılı liste düğümü ReadOnlySequenceSegment<T> ve dizini çifti.

Üçüncü gösterim, ReadOnlySequence<T> üzerindeki çeşitli işlemlerde performans üzerindeki etkileri nedeniyle en ilginç olanıdır.

Temsil Operasyon Karmaşıklık
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)

Bu karma gösterim nedeniyle dizinleri ReadOnlySequence<T> tamsayı yerine olarak SequencePosition kullanıma sunar. A SequencePosition:

  • Kaynağında bulunduğu ReadOnlySequence<T> içine bir dizini temsil eden opak bir değerdir.
  • Bir tamsayı ve bir nesne olmak üzere iki bölümden oluşur. Bu iki değerin temsili, uygulamasına ReadOnlySequence<T>bağlıdır.

Verilere erişme

ReadOnlySequence<T> verileri bir ReadOnlyMemory<T> dizisi olarak sunar. Segmentlerin her birini listelemek temel bir foreach kullanılarak yapılabilir:

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

Yukarıdaki yöntem her segmentte belirli bir bayt arar. Her bir kesimi SequencePosition izlemeniz gerekiyorsa, ReadOnlySequence<T>.TryGet daha uygundur. Sonraki örnek, önceki kodu tamsayı yerine bir SequencePosition döndürecek şekilde değiştirir. SequencePosition öğesini döndürmek, çağıranın belirli bir indeksteki verileri almak için ikinci bir taramadan kaçınmasını sağlar.

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 ve TryGet birleşimi bir numaralandırıcı gibi davranır. Konum alanı, her yinelemenin başında içindeki her segmentin ReadOnlySequence<T>başlangıcı olacak şekilde değiştirilir.

Yukarıdaki yöntem, üzerinde ReadOnlySequence<T>bir uzantı yöntemi olarak bulunur. PositionOf yukarıdaki kodu basitleştirmek için kullanılabilir:

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

ReadOnlySequence<T'yi İşle>

Veriler dizi içindeki birden çok segmente bölünebileceğinden, bir ReadOnlySequence<T> işlenme zor olabilir. En iyi performans için kodu iki yola bölün:

  • Tek segment durumuyla ilgilenen hızlı bir çözüm.
  • Segmentler arasında bölünmüş verilerle ilgilenen yavaş bir yol.

Çok segmentli dizilerdeki verileri işlemek için kullanılabilecek birkaç yaklaşım vardır:

  • kullanın SequenceReader<T>.
  • Segment segment veri parçalarını ayrıştırırken, SequencePosition ve ayrıştırılan segment içindeki dizini takip edin. Bu, gereksiz ayırmaları önler, ancak özellikle küçük arabellekler için verimsiz olabilir.
  • Veri ReadOnlySequence<T>'yi bitişik bir diziye kopyalayın ve onu tek bir arabellek olarak işleyin.
    • Eğer ReadOnlySequence<T> boyutu küçükse, verileri stackalloc işlecini kullanarak yığın üzerinde ayrılmış bir arabelleğe kopyalamak mantıklı olabilir.
    • ReadOnlySequence<T> öğesini ArrayPool<T>.Shared kullanarak havuza alınmış bir diziye kopyalayın.
    • ReadOnlySequence<T>.ToArray()kullanın. Yığında yeni T[] bir ayırma yaptığı için sık erişimli yollarda bu önerilmez.

Aşağıdaki örneklerde işlemeye yönelik bazı yaygın durumlar gösterilmektedir ReadOnlySequence<byte>:

İkili verileri işleme

Aşağıdaki örnek, ReadOnlySequence<byte>'in başından 4 baytlık büyük endian tamsayının uzunluğunu ayrıştırmaktadır.

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;
}
Metin verilerini işleme

Aşağıdaki örnek:

  • \r\n içindeki ilk yeni satırı (ReadOnlySequence<byte>) bulur ve bunu 'line' çıkış parametresiyle döndürür.
  • Giriş arabelleğinden \r\n hariç tutularak bu satır kırpılır.
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;
}
Boş segmentler

Bir ReadOnlySequence<T> içinde boş segmentleri depolamak geçerlidir. Kesimler açıkça numaralandırılıyorken boş segmentler oluşabilir:

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

Yukarıdaki kod, boş kesimler içeren bir ReadOnlySequence<byte> oluşturur ve bu boş segmentlerin çeşitli API'leri nasıl etkilediğini gösterir:

  • ReadOnlySequence<T>.Slice boş bir segmente işaret eden bir SequencePosition kesim ile bu segment korunur.
  • ReadOnlySequence<T>.Slice int ile boş kesimleri atlar.
  • "ReadOnlySequence<T>'u numaralandırmak, boş segmentleri numaralandırır."

ReadOnlySequence<T> ve SequencePosition ile ilgili olası sorunlar

Bir ReadOnlySequence<T>/SequencePosition ile normal bir ReadOnlySpan<T>/ReadOnlyMemory<T>/T[]/int karşısında birkaç olağan dışı sonuç vardır:

  • SequencePosition, mutlak bir konum için değil, belirli bir ReadOnlySequence<T> konum işaretçisidir. Belirli bir ReadOnlySequence<T>'ye göre olmasından dolayı, orijinal ReadOnlySequence<T>'ün dışında kullanıldığında anlamını yitirir.
  • SequencePosition üzerinde aritmetik, ReadOnlySequence<T> olmadan gerçekleştirilemez. Bu, position++ gibi temel şeylerin position = ReadOnlySequence<T>.GetPosition(1, position) yazılması anlamına gelir.
  • GetPosition(long) negatif dizinleri desteklemez . Bu, tüm segmentleri geçmeden sondan ikinci karakteri almak mümkün olmadığı anlamına gelir.
  • İkisi SequencePosition karşılaştırılamaz ve bu da şunları zorlaştırıyor:
    • Bir konumun başka bir konumdan büyük veya daha küçük olup olmadığını bilme.
    • Bazı ayrıştırma algoritmaları yazın.
  • ReadOnlySequence<T> bir nesne referansından daha büyüktür ve mümkün olduğunca in veya ref ile geçirilmelidir. ReadOnlySequence<T>'yi in veya ref ile geçmek, yapı kopyalarını azaltır.
  • Boş segmentler:
    • Bir ReadOnlySequence<T> içinde geçerlidir.
    • yöntemi kullanılarak ReadOnlySequence<T>.TryGet yinelenirken görüntülenebilir.
    • ReadOnlySequence<T>.Slice() yöntemi ve SequencePosition nesneleri kullanarak sırayı dilimleme işlemi görünebilir.

SequenceReader<T>

SequenceReader<T>:

  • Bir ReadOnlySequence<T> işlemesini basitleştirmek için .NET Core 3.0'da tanıtılan yeni bir türdür.
  • Tek segment ReadOnlySequence<T> ile çok segment ReadOnlySequence<T> arasındaki farklılıkları birleştirir.
  • Bölümler arasında bölünebilen veya bölünemeyebilir ikili verileri ve metin verilerini (byte ve char) okumak için yardımcılar sağlar.

hem ikili hem de sınırlandırılmış verileri işlemek için yerleşik yöntemler vardır. Aşağıdaki bölümde, SequenceReader<T> ile aynı yöntemlerin nasıl göründüğü gösterilmektedir.

Verilere erişme

SequenceReader<T>, ReadOnlySequence<T> içinde verileri listelemek için yöntemlere sahiptir. Aşağıdaki kod, bir ReadOnlySequence<byte> ve bir byte'in bir kerede işlenmesi örneğidir.

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

CurrentSpan yöntemde manuel olarak yapılana benzer şekilde mevcut segmentin Span öğesini kullanıma sunar.

Konumunu kullan

Aşağıdaki kod, FindIndexOf'ün, SequenceReader<T> kullanılarak örnek bir uygulamasıdır.

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

İkili verileri işleme

Aşağıdaki örnek, ReadOnlySequence<byte>'in başından 4 baytlık büyük endian tamsayının uzunluğunu ayrıştırmaktadır.

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

Metin verilerini işleme

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> yaygın sorunları

  • SequenceReader<T> Değiştirilebilir bir yapı olduğundan, her zaman başvuru ile geçirilmelidir.
  • SequenceReader<T> bir başvuru yapısıdır , bu nedenle yalnızca zaman uyumlu yöntemlerde kullanılabilir ve alanlarda depolanamaz. Daha fazla bilgi için Tahsisatlardan kaçınma bölümüne bakın.
  • SequenceReader<T> , yalnızca ileriye doğru okuyucu olarak kullanılmak üzere en iyi duruma getirilmiştir. Rewind, diğer Read, Peek ve IsNext API'ler kullanılarak ele alınamayan küçük yedeklemeler için tasarlanmıştır.