Číst v angličtině

Sdílet prostřednictvím


Práce s vyrovnávacími paměťmi v .NET

Tento článek obsahuje přehled typů, které pomáhají číst data, která běží ve více vyrovnávacích pamětích. Primárně se používají k podpoře PipeReader objektů.

IBufferWriter<T>

System.Buffers.IBufferWriter<T> je kontrakt pro synchronní zápis do vyrovnávací paměti. Na nejnižší úrovni rozhraní:

  • Je základní a není obtížné ji používat.
  • Umožňuje přístup k nebo Memory<T>Span<T>. Můžete Memory<T> je napsat nebo Span<T> můžete určit, T kolik položek bylo zapsáno.
C#
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);
}

Předchozí metoda:

  • Požaduje vyrovnávací paměť nejméně 5 bajtů z IBufferWriter<byte> použití GetSpan(5).
  • Zapíše bajty pro řetězec ASCII "Hello" na vrácený Span<byte>.
  • Volání IBufferWriter<T> označující, kolik bajtů bylo zapsáno do vyrovnávací paměti.

Tato metoda zápisu Memory<T>/Span<T> používá vyrovnávací paměť poskytovanou .IBufferWriter<T> Alternativně lze metodu Write rozšíření použít ke zkopírování existující vyrovnávací paměti do IBufferWriter<T>. Writefunguje volání GetSpan/Advance podle potřeby, takže po napsání není nutné volat:Advance

C#
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> je implementace, jejíž IBufferWriter<T> záložní úložiště je jedno souvislé pole.

IBufferWriter – běžné problémy

  • GetSpan a GetMemory vrátit vyrovnávací paměť s alespoň požadovanou velikostí paměti. Nepředpokládáme přesné velikosti vyrovnávací paměti.
  • Není zaručeno, že následná volání vrátí stejnou vyrovnávací paměť nebo vyrovnávací paměť stejné velikosti.
  • Po volání Advance musí být požadována nová vyrovnávací paměť, aby bylo možné pokračovat v zápisu dalších dat. Dříve získanou vyrovnávací paměť nelze zapsat do po Advance zavolání.

ReadOnlySequence<T>

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

ReadOnlySequence<T> je struktura, která může představovat souvislou nebo nesouvisenou sekvenci T. Dá se vytvořit z:

  1. Provede T[].
  2. Provede ReadOnlyMemory<T>.
  3. Dvojice propojených uzlů ReadOnlySequenceSegment<T> seznamu a indexu, která představuje počáteční a koncovou pozici sekvence.

Třetí reprezentace je nejzajímavější, protože má vliv na výkon na různé operace na ReadOnlySequence<T>:

Reprezentace Operace Složitost
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)

Z důvodu této smíšené reprezentace ReadOnlySequence<T> zveřejňuje indexy místo SequencePosition celého čísla. A SequencePosition:

  • Je neprůhledná hodnota, která představuje index do ReadOnlySequence<T> místa, kde pochází.
  • Skládá se ze dvou částí, celého čísla a objektu. Jaké tyto dvě hodnoty představují, jsou svázané s implementací ReadOnlySequence<T>.

Přístup k datům

Zpřístupňuje ReadOnlySequence<T> data jako výčet .ReadOnlyMemory<T> Výčet jednotlivých segmentů lze provést pomocí základního foreachu:

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

Předchozí metoda hledá v každém segmentu konkrétní bajt. Pokud potřebujete sledovat jednotlivé segmenty SequencePosition, ReadOnlySequence<T>.TryGet je vhodnější. Další ukázka změní předchozí kód tak, aby vrátil SequencePosition místo celého čísla. Vrácení SequencePosition má výhodu, že volajícímu umožníte vyhnout se druhému prohledávání, abyste získali data v určitém indexu.

C#
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 Kombinace enumerátoru a TryGet funguje jako enumerátor. Pole pozice je změněno na začátku každé iterace tak, aby začínalo u každého segmentu v rámci ReadOnlySequence<T>.

Předchozí metoda existuje jako rozšiřující metoda pro ReadOnlySequence<T>. PositionOf lze použít ke zjednodušení předchozího kódu:

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

Zpracování operace ReadOnlySequence<T>

ReadOnlySequence<T> Zpracování může být náročné, protože data mohou být rozdělena mezi více segmentů v rámci sekvence. Nejlepšího výkonu dosáhnete rozdělením kódu do dvou cest:

  • Rychlá cesta, která se zabývá jedním segmentovým případem.
  • Pomalá cesta, která se zabývá rozdělením dat mezi segmenty.

Existuje několik přístupů, které je možné použít ke zpracování dat ve více segmentovaných sekvencích:

  • Použijte .SequenceReader<T>
  • Parsujte datový segment podle segmentů a sledujte index SequencePosition v rámci analyzovaného segmentu. Tím se vyhnete zbytečným přidělením, ale může to být neefektivní, zejména u malých vyrovnávacích pamětí.
  • Zkopírujte ho ReadOnlySequence<T> do souvislého pole a zacházejte s ním jako s jednou vyrovnávací pamětí:
    • Pokud je velikost ReadOnlySequence<T> malé, může být vhodné data zkopírovat do vyrovnávací paměti přidělené zásobníku pomocí operátoru stackalloc .
    • Zkopírujte do ReadOnlySequence<T> pole ve fondu pomocí .ArrayPool<T>.Shared
    • Použijte ReadOnlySequence<T>.ToArray(). Nedoporučuje se to v horkých cestách, protože přiděluje novou T[] haldu.

Následující příklady ukazují některé běžné případy zpracování ReadOnlySequence<byte>:

Zpracování binárních dat

Následující příklad analyzuje celočíselnou délku 4bajtů big-endian od začátku ReadOnlySequence<byte>.

C#
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;
}
Zpracování textových dat

Následující příklad:

  • Vyhledá první nový řádek (\r\n) v sadě ReadOnlySequence<byte> a vrátí ho prostřednictvím parametru out 'line'.
  • Oříznou tento řádek s výjimkou \r\n vstupní vyrovnávací paměti.
C#
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;
}
Prázdné segmenty

Je platné ukládat prázdné segmenty uvnitř objektu ReadOnlySequence<T>. Při explicitní výčtu segmentů může dojít k prázdným segmentům:

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

Předchozí kód vytvoří ReadOnlySequence<byte> s prázdnými segmenty a ukáže, jak tyto prázdné segmenty ovlivňují různá rozhraní API:

  • ReadOnlySequence<T>.SliceSequencePosition odkazující na prázdný segment zachová tento segment.
  • ReadOnlySequence<T>.Slice funkce int přeskočí prázdné segmenty.
  • Výčet výčtů ReadOnlySequence<T> vytvoří výčty prázdných segmentů.

Potenciální problémy s readOnlySequence<T> a SequencePosition

Při práci s vsReadOnlySequence<T>SequencePosition/. normálnímReadOnlyMemory<T>/intReadOnlySpan<T>/T[]/ výsledkem je několik neobvyklých výsledků:

  • SequencePosition je značka pozice pro konkrétní ReadOnlySequence<T>, nikoli absolutní pozici. Vzhledem k tomu, že je relativní ke konkrétnímu ReadOnlySequence<T>, nemá význam, pokud se používá mimo ReadOnlySequence<T> místo, kde pochází.
  • Aritmetika nemůže být provedena SequencePosition bez ReadOnlySequence<T>. To znamená, že dělat základní věci, jako position++ je napsán position = ReadOnlySequence<T>.GetPosition(1, position).
  • GetPosition(long)nepodporuje záporné indexy. To znamená, že není možné získat druhý až poslední znak bez chůze všech segmentů.
  • Dva SequencePosition se nedají porovnat, což ztěžuje:
    • Zjistěte, jestli je jedna pozice větší nebo menší než jiná pozice.
    • Napište několik algoritmů analýzy.
  • ReadOnlySequence<T>je větší než odkaz na objekt a měl by být předán v nebo ref, pokud je to možné. Předáním ReadOnlySequence<T> nebo ref zmenšením in kopií struktury.
  • Prázdné segmenty:
    • Jsou platné v rámci .ReadOnlySequence<T>
    • Může se zobrazit při iteraci pomocí ReadOnlySequence<T>.TryGet metody.
    • Může se zobrazit řez sekvence pomocí ReadOnlySequence<T>.Slice() metody s SequencePosition objekty.

SequenceReader<T>

SequenceReader<T>:

  • Je nový typ, který byl zaveden v .NET Core 3.0 pro zjednodušení zpracování ReadOnlySequence<T>.
  • Sjednocuje rozdíly mezi jedním segmentem ReadOnlySequence<T> a více segmenty ReadOnlySequence<T>.
  • Poskytuje pomocné rutiny pro čtení binárních a textových dat (byte a char) , které mohou nebo nemusí být rozděleny mezi segmenty.

Existují integrované metody pro zpracování binárních i oddělených dat. Následující část ukazuje, jak tyto stejné metody vypadají pomocí SequenceReader<T>:

Přístup k datům

SequenceReader<T> obsahuje metody pro vytvoření výčtu dat uvnitř ReadOnlySequence<T> přímo. Následující kód je příkladem zpracování ReadOnlySequence<byte>byte najednou:

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

Zveřejňuje CurrentSpan aktuální segment Span, který je podobný tomu, co bylo provedeno v metodě ručně.

Použít pozici

Následující kód je příkladem implementace FindIndexOf použití SequenceReader<T>:

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

Zpracování binárních dat

Následující příklad analyzuje celočíselnou délku 4bajtů big-endian od začátku ReadOnlySequence<byte>.

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

Zpracování textových dat

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

Běžné problémy se sequenceReader<T>

  • Protože SequenceReader<T> je proměnlivá struktura, měla by být vždy předána odkazem.
  • SequenceReader<T>je ref struktura, takže ji lze použít pouze v synchronních metodách a nelze ji uložit do polí. Další informace najdete v tématu Vyhněte se přidělování.
  • SequenceReader<T> je optimalizovaná pro použití jako čtečka určená jen pro předávání. Rewindje určený pro malé zálohy, které nelze řešit pomocí jiných ReadPeekrozhraní API a IsNext rozhraní API.