Lire en anglais

Partager via


Utiliser des mémoires tampons dans .NET

Cet article fournit une vue d’ensemble des types qui facilitent la lecture des données qui se trouvent dans plusieurs mémoires tampons. Ils sont principalement utilisés pour prendre en charge des objets PipeReader.

IBufferWriter<T>

System.Buffers.IBufferWriter<T> est un contrat pour l’écriture en mémoire tampon synchrone. Au niveau le plus bas, l’interface :

  • Est une interface de base et n’est pas difficile à utiliser.
  • Permet l’accès à un Memory<T> ou un .Span<T> Le Memory<T> ou le Span<T> est accessible en écriture et vous pouvez déterminer le nombre d’éléments T qui y ont été écrits.
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);
}

La méthode précédente :

  • Demande une mémoire tampon d’au moins 5 octets auprès du IBufferWriter<byte> en utilisant GetSpan(5).
  • Écrit des octets pour la chaîne ASCII « Hello » dans le Span<byte> retourné.
  • Appelle IBufferWriter<T> pour indiquer le nombre d’octets qui ont été écrits dans la mémoire tampon.

Cette méthode d’écriture utilise la mémoire tampon Memory<T>/Span<T> fournie par le IBufferWriter<T>. Vous pouvez aussi utiliser la méthode d’extension Write pour copier une mémoire tampon existante dans le IBufferWriter<T>. Write effectue le travail consistant à appeler GetSpan/Advance de façon appropriée : il n’est donc pas nécessaire d’appeler Advance après l’écriture :

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> est une implémentation de IBufferWriter<T> qui utilise comme magasin un seul tableau contigu.

Problèmes courants liés à IBufferWriter

  • GetSpan et GetMemory retournent une mémoire tampon avec au moins la quantité de mémoire demandée. Les tailles des mémoires tampons ne seront pas nécessairement exactes.
  • Il n’est pas garanti que des appels successifs vont retourner la même mémoire tampon ou une mémoire tampon de même taille.
  • Une nouvelle mémoire tampon doit être demandée après l’appel de Advance pour continuer à écrire d’autres données. Vous ne pouvez pas écrire dans une mémoire tampon précédemment acquise après avoir appelé Advance.

ReadOnlySequence<T>

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

ReadOnlySequence<T> est un struct qui peut représenter une séquence contiguë ou non contiguë de T. Il peut être construit à partir de :

  1. T[]
  2. ReadOnlyMemory<T>
  3. Une paire constituée de ReadOnlySequenceSegment<T> d’un nœud de liste lié et d’un index pour représenter la position de début et de fin de la séquence.

La troisième représentation est la plus intéressante, car elle a des implications en termes de performances sur les différentes opérations effectuées sur la ReadOnlySequence<T> :

Représentation Opération Complexité
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)

En raison de cette représentation mixte, la ReadOnlySequence<T> expose les index sous la forme d’une SequencePosition au lieu d’un entier. Une SequencePosition :

  • Est une valeur opaque qui représente un index dans la ReadOnlySequence<T> dont elle provient.
  • Se compose de deux parties, un entier et un objet. Ce que représentent ces deux valeurs est lié à l’implémentation de ReadOnlySequence<T>.

Accéder aux données

La ReadOnlySequence<T> expose les données sous la forme d’un énumérable de ReadOnlyMemory<T>. L’énumération de chacun des segments peut être effectuée en utilisant une instruction foreach de base :

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

La méthode précédente recherche un octet spécifique dans chaque segment. Si vous devez effectuer le suivi de la SequencePosition de chaque segment, ReadOnlySequence<T>.TryGet est plus approprié. L’exemple suivant est le code précédent modifié pour retourner une SequencePosition au lieu d’un entier. Retourner une SequencePosition a l’avantage de permettre à l’appelant d’éviter de devoir parcourir une deuxième fois la séquence pour obtenir les données à un index spécifique.

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

La combinaison de SequencePosition et de TryGet agit comme un énumérateur. Le champ de position est modifié au début de chaque itération de façon à refléter le début de chaque segment dans la ReadOnlySequence<T>.

La méthode précédente existe en tant que méthode d’extension sur ReadOnlySequence<T>. PositionOf peut être utilisé pour simplifier le code précédent :

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

Traiter une ReadOnlySequence<T>

Le traitement d’une ReadOnlySequence<T> peut être problématique, car les données peuvent être fractionnées sur plusieurs segments au sein de la séquence. Pour de meilleures performances, divisez le code en deux chemins :

  • Un chemin rapide qui traite le cas d’un seul segment.
  • Un chemin lent qui traite le fractionnement des données entre des segments.

Quelques approches différentes peuvent être utilisées pour traiter des données dans des séquences sur plusieurs segments :

  • Utilisez la méthode SequenceReader<T>.
  • Analyser les données segment par segment, en effectuant le suivi de la SequencePosition et de l’index dans le segment analysé. Ceci évite des allocations non nécessaires, mais peut être inefficace, en particulier pour les mémoires tampons de petite taille.
  • Copier la ReadOnlySequence<T> dans un tableau contigu et le traiter comme une même mémoire tampon :
    • Si la taille de la ReadOnlySequence<T> est petite, il peut être raisonnable de copier les données dans une mémoire tampon allouée par la pile en utilisant l’opérateur stackalloc.
    • Copier la ReadOnlySequence<T> dans un tableau mis en pool en utilisant ArrayPool<T>.Shared.
    • Utilisez ReadOnlySequence<T>.ToArray(). Ceci n’est pas recommandé dans les chemins très sollicités, car cela alloue un nouveau T[] sur le tas.

Les exemples suivants illustrent quelques cas courants de traitement de ReadOnlySequence<byte> :

Traiter des données binaires

L’exemple suivant analyse la longueur d’un entier Big Endian sur 4 octets à partir du début de la 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;
}
Traiter des données texte

L’exemple suivant :

  • Recherche le premier saut de ligne (\r\n) dans la ReadOnlySequence<byte> et le retourne via le paramètre de sortie « line ».
  • Supprime cette ligne, en excluant le \r\n de la mémoire tampon d’entrée.
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;
}
Segments vides

Stocker des segments vides à l’intérieur d’une ReadOnlySequence<T> est une action valide. Des segments vides peuvent se produire lors de l’énumération explicite de segments :

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

Le code précédent crée une ReadOnlySequence<byte> avec des segments vides et montre comment ils affectent les différentes API :

  • ReadOnlySequence<T>.Slice avec une SequencePosition pointant vers un segment vide conserve ce segment.
  • ReadOnlySequence<T>.Slice avec un entier (int) passe les segments vides.
  • L’énumération de la ReadOnlySequence<T> énumère les segments vides.

Problèmes potentiels avec ReadOnlySequence<T> et SequencePosition

Plusieurs résultats inhabituels peuvent se produire lors de l’utilisation d’une ReadOnlySequence<T>/SequencePosition par rapport à un ReadOnlySpan<T>/ReadOnlyMemory<T>/T[]/int normal :

  • SequencePosition est un marqueur de position pour une ReadOnlySequence<T> spécifique, et non pas une position absolue. Étant donné qu’il est relatif à une ReadOnlySequence<T> spécifique, il n’a pas de signification s’il est utilisé en dehors de la ReadOnlySequence<T> dont il provient.
  • Vous ne pouvez pas effectuer des opérations arithmétiques sur SequencePosition sans la ReadOnlySequence<T>. Cela signifie que faire des opérations de base comme position++ doit être écrit comme ceci : position = ReadOnlySequence<T>.GetPosition(1, position).
  • GetPosition(long) ne prend pas en charge les index négatifs. Cela signifie qu’il est impossible d’obtenir l’avant-dernier caractère sans parcourir tous les segments.
  • Il n’est pas possible de comparer deux SequencePosition, ce qui rend difficile :
    • De savoir si une position est supérieure ou inférieure à une autre position.
    • D’écrire des algorithmes d’analyse.
  • ReadOnlySequence<T> est plus grande qu’une référence d’objet, et doit être passée via in ou ref là où c’est possible. Passer ReadOnlySequence<T> via in ou ref réduit les copies du struct.
  • Les segments vides :
    • Sont valides dans une ReadOnlySequence<T>.
    • Peuvent apparaître lors de l’itération en utilisant la méthode ReadOnlySequence<T>.TryGet.
    • Peuvent fractionner la séquence en utilisant la méthode ReadOnlySequence<T>.Slice() avec des objets SequencePosition.

SequenceReader<T>

SequenceReader<T>:

  • Est un nouveau type introduit dans .NET Core 3.0 pour simplifier le traitement d’une ReadOnlySequence<T>.
  • Unifie les différences entre une ReadOnlySequence<T> avec un seul segment et une ReadOnlySequence<T> avec plusieurs segments.
  • Fournit des assistances pour la lecture de données binaires et texte (byte et char) qui peuvent ou non être fractionnées sur plusieurs segments.

Il existe des méthodes intégrées pour traiter à la fois les données binaires et les données délimitées. La section suivante montre à quoi ressemblent ces mêmes méthodes avec le SequenceReader<T> :

Accéder aux données

SequenceReader<T> a des méthodes pour énumérer des données directement à l’intérieur de la ReadOnlySequence<T>. Le code suivant est un exemple de traitement d’une ReadOnlySequence<byte> un byte à la fois :

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

CurrentSpan expose le Span du segment actif, ce qui est similaire à ce qui était effectué manuellement dans la méthode.

Utiliser une position

Le code suivant est un exemple d’implémentation de FindIndexOf en utilisant le 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;
}

Traiter des données binaires

L’exemple suivant analyse la longueur d’un entier Big Endian sur 4 octets à partir du début de la ReadOnlySequence<byte>.

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

Traiter des données texte

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

Problèmes courants liés à SequenceReader<T>

  • Étant donné que SequenceReader<T> est un struct mutable, il doit toujours être passé par référence.
  • SequenceReader<T> est un ref struct : il peut être donc être utilisé seulement dans des méthodes synchrones et ne peut pas être stocké dans des champs. Pour plus d’informations, consultez Éviter les allocations.
  • SequenceReader<T> est optimisé pour une utilisation en tant que lecteur vers l’avant uniquement. Rewind est destiné aux petites sauvegardes qui ne peuvent pas être traitées en utilisant d’autres API Read, Peek et IsNext.