Pufferek működése a .NET-ben

Ez a cikk áttekintést nyújt azokról a típusokról, amelyek segítenek a több pufferben futó adatok olvasásában. Elsősorban PipeReader objektumok támogatására szolgálnak.

IBufferWriter<T>

System.Buffers.IBufferWriter<T> szinkron pufferelt írásra vonatkozó szerződés. A legalacsonyabb szinten a felület:

  • Alapszintű és nem nehéz használni.
  • Engedélyezi a hozzáférést egy Memory<T> vagy Span<T>. A Memory<T> vagy Span<T> megírható, és meghatározhatja, hogy hány T elemet írtak.
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);
}

Az előző módszer:

  • Kér egy legalább 5 bájtos puffert a IBufferWriter<byte>-tól a GetSpan(5) használatával.
  • ASCII "Hello" karakterlánc bájtjait írja az így kapott Span<byte> elemre.
  • Hívások IBufferWriter<T> a pufferbe írt bájtok számának jelzésére.

Ez az írási módszer a Memory<T>/Span<T> puffer használatát alkalmazza, amelyet a IBufferWriter<T> biztosít. Másik lehetőségként a Write kiterjesztési metódus használható egy meglévő puffer másolására a IBufferWriter<T>-be. Write a megfelelő módon hívja meg a GetSpan/Advance-t, így nincs szükség külön Advance hívására írás után.

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> annak a implementációja IBufferWriter<T> , amelynek háttértárolója egyetlen egybefüggő tömb.

Az IBufferWriter gyakori problémái

  • GetSpan és GetMemory adjon vissza egy puffert legalább a kért memóriamennyiséggel. Ne feltételezze a puffer pontos méretét.
  • Nincs garancia arra, hogy az egymást követő hívások ugyanazt a puffert vagy azonos méretű puffert fogják visszaadni.
  • A további adatok írásának folytatásához a hívás Advance után új puffert kell kérni. Egy korábban megszerzett puffer nem írható, miután a Advance hívásra került.

ReadOnlySequence<T>

ReadOnlySequence, amely a memóriát mutatja a csatornában, majd alatta az írásvédett memória sorozathelyzetét

ReadOnlySequence<T> egy struktúra, amely egy egybefüggő vagy nem folytonos sorozatot jelölhet T-ből. Az alábbi elemekből építhető ki:

  1. Egy T[]
  2. Egy ReadOnlyMemory<T>
  3. Csatolt listacsomópont ReadOnlySequenceSegment<T> és indexpár, amely a sorozat kezdő és záró pozícióját jelöli.

A harmadik ábrázolás a legérdekesebb, mivel teljesítménybeli hatásokkal van a különböző műveletekre a ReadOnlySequence<T> esetében.

Képviselet Művelet Összetettség
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)

A vegyes ábrázolás miatt az ReadOnlySequence<T> indexeket egész szám helyett SequencePosition teszi elérhetővé. A SequencePosition:

  • Átlátszatlan érték, amely egy indexet jelöl a ReadOnlySequence<T> kiindulási helyre.
  • Két részből, egy egész számból és egy objektumból áll. Az a két érték kapcsolódik a ReadOnlySequence<T> végrehajtásához.

Adatok elérése

A ReadOnlySequence<T> az adatokat ReadOnlyMemory<T> felsorolhatóként teszi elérhetővé. Az egyes szegmensek számbavétele egy alapszintű foreach használatával végezhető el:

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

Az előző metódus az egyes szegmensekben keres egy adott bájtot. Ha minden egyes szegmenst nyomon kell követnie, akkor a SequencePosition célszerűbb. A következő minta úgy módosítja az előző kódot, hogy egész szám helyett egy SequencePosition értéket adjon vissza. A visszaadott adatok SequencePosition előnye, hogy lehetővé teszik a hívó számára, hogy elkerülje a második vizsgálatot az adatok adott indexben való lekéréséhez.

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

A kombináció, SequencePosition és TryGet úgy viselkedik, mint egy enumerátor. A pozíciómező az egyes iterációk elején módosul, hogy az egyes szegmensek kezdetét jelezze a ReadOnlySequence<T>-ben.

Az előző metódus bővítménymetódusként létezik a következőn ReadOnlySequence<T>: . PositionOf az előző kód egyszerűsítése érdekében használható:

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

Feldolgozza a ReadOnlySequence<T> rendszert

Egy ReadOnlySequence<T> feldolgozása kihívást jelenthet, mivel az adatok lehet, hogy több szegmensre oszlanak a sorozaton belül. A legjobb teljesítmény érdekében ossza fel a kódot két útvonalra:

  • Egy gyors útvonal, amely az egyetlen szegmenses esettel foglalkozik.
  • Lassú folyamat, amely a szegmensek közötti adatmegosztással foglalkozik.

Az adatok többszegmenses szekvenciákban történő feldolgozásához néhány módszer használható:

  • Használja a SequenceReader<T>.
  • Elemezze az adatszegmenseket szegmensen ként, nyomon követve az SequencePosition pozícióját és az elemzett szegmensen belüli indexet. Ez elkerüli a szükségtelen foglalásokat, de nem hatékony, különösen a kis pufferek esetében.
  • Másolja a ReadOnlySequence<T> tömböt egy összefüggő tömbbe, és kezelje egyetlen pufferként:
    • Ha a ReadOnlySequence<T> mérete kicsi, ésszerű lehet az adatokat egy veremre lefoglalt pufferbe másolni a stackalloc operátor segítségével.
    • Másolja a ReadOnlySequence<T> a ArrayPool<T>.Shared segítségével a készletezett tömbbe.
    • Használja a ReadOnlySequence<T>.ToArray(). Ez nem ajánlott a gyakori elérésű útvonalakon, mivel újat T[] foglal le a halomra.

Az alábbi példák a feldolgozás ReadOnlySequence<byte>néhány gyakori esetét szemléltetik:

Bináris adatok feldolgozása

Az alábbi példa egy 4 bájtos big-endian egész szám hosszát értelmezi a ReadOnlySequence<byte> elejétől.

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;
}
Szövegadatok feldolgozása

A következő példa:

  • Megkeresi az első új sort (\r\n) a ReadOnlySequence<byte>-ben, és a 'line' kimeneti paraméteren keresztül adja vissza.
  • Eltávolítja a sort úgy, hogy a \r\n-t kihagyja a bemeneti pufferből.
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;
}
Üres szegmensek

Érvényes üres szegmensek tárolása egy ReadOnlySequence<T>-ben. Üres szegmensek fordulhatnak elő a szegmensek explicit számbavétele során:

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

Az előző kód létrehoz egy ReadOnlySequence<byte> üres szegmenseket, és bemutatja, hogy ezek az üres szegmensek hogyan befolyásolják a különböző API-kat:

  • ReadOnlySequence<T>.Slice ha egy SequencePosition üres szegmensre mutat, az megőrzi ezt a szegmenst.
  • ReadOnlySequence<T>.Slice egy inttel átugorjuk az üres szegmenseket.
  • A ReadOnlySequence<T> felsorolja az üres szegmenseket.

A ReadOnlySequence<T> és a SequencePosition lehetséges problémái

A ReadOnlySequence<T>/SequencePosition és egy normál ReadOnlySpan<T>/ReadOnlyMemory<T>/T[]/int összehasonlításakor számos szokatlan eredmény érhető el.

  • SequencePosition egy adott ReadOnlySequence<T>, nem abszolút pozícióhoz tartozó pozíciójelölő. Mivel egy adotthoz ReadOnlySequence<T>viszonyítva van, nincs értelme, ha a ReadOnlySequence<T> kiindulási helyén kívül használják.
  • Az aritmetikai műveletek nem végezhetők el SequencePosition nélkül a ReadOnlySequence<T>. Ez azt jelenti, hogy alapvető dolgokat, mint például position++, így position = ReadOnlySequence<T>.GetPosition(1, position) írni.
  • GetPosition(long) nem támogatja a negatív indexeket. Ez azt jelenti, hogy lehetetlen megszerezni az utolsó előtti karaktert anélkül, hogy végigjárnánk az összes szakaszt.
  • Kettő SequencePosition nem hasonlítható össze, ami megnehezíti a következőt:
    • Annak megállapítása, hogy az egyik helyzet nagyobb vagy kisebb, mint egy másik helyzet.
    • Írjon elemzési algoritmusokat.
  • ReadOnlySequence<T> nagyobb, mint egy objektumhivatkozás, és ahol lehetséges, in vagy ref alapján kell átadni. Átadás ReadOnlySequence<T>in vagy ref csökkenti a szerkezet másolatait.
  • Üres szegmensek:
    • Érvényes ReadOnlySequence<T>-on belül.
    • Az iterálás során a ReadOnlySequence<T>.TryGet metódus használatakor megjelenhet.
    • A sorozat szeletelése a ReadOnlySequence<T>.Slice() módszerrel SequencePosition objektumok segítségével jelenhet meg.

SequenceReader<T>

SequenceReader<T>:

  • A .NET Core 3.0-ban bevezetett egy új típus egyszerűsíti a ReadOnlySequence<T> feldolgozását.
  • Egyesíti az egy szegmens és a több szegmens ReadOnlySequence<T>ReadOnlySequence<T>közötti különbségeket.
  • Segítséget nyújt olyan bináris és szöveges adatok (byte és char) olvasásához, amelyek szegmensekre oszthatók vagy nem oszthatók fel.

A bináris és a tagolt adatok feldolgozásának kezelésére beépített módszerek is léteznek. Az alábbi szakasz megmutatja, hogyan néznek ki ezek a metódusok a SequenceReader<T> használatával.

Adatok elérése

A SequenceReader<T>-nak vannak metódusai az adatok közvetlen számbavételére a ReadOnlySequence<T>-ban. Az alábbi kód egy példa arra, hogyan dolgozzunk fel egyszerre egy ReadOnlySequence<byte> és egy byte:

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

A CurrentSpan művelet az aktuális szegmenseket Spanteszi elérhetővé, ami hasonló a módszer manuális műveletéhez.

Pozíció használata

Az alábbi kód egy példa a FindIndexOf implementálására a SequenceReader<T> használatával.

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

Bináris adatok feldolgozása

Az alábbi példa egy 4 bájtos big-endian egész szám hosszát értelmezi a ReadOnlySequence<byte> elejétől.

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

Szövegadatok feldolgozása

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> – gyakori problémák

  • Mivel SequenceReader<T> ez egy mutable struct, azt mindig hivatkozással kell átadni.
  • SequenceReader<T> egy refstruct , így csak szinkron metódusokban használható, és nem tárolható mezőkben. További információért lásd: A foglalások elkerülése.
  • SequenceReader<T> csak továbbítható olvasóként való használatra van optimalizálva. Rewindolyan kis biztonsági mentésekhez készült, amelyek más , Readés Peek API-k használatával IsNextnem kezelhetők.