Chargement...

Optimisation des performances avec Span<T> et Memory<T> en C#

Apprenez l’optimisation des performances en C# avec Span<T> et Memory<T> pour une gestion mémoire efficace.

Introduits avec C# 7.2 et les versions ultérieures, les types Span<T> et Memory<T> optimisent l’accès à la mémoire dans les scénarios de traitement de données haute performance, en évitant les copies inutiles. Grâce à ces structures, vous pouvez manipuler de grands tableaux, chaînes de caractères ou tampons d’octets sans allocation de mémoire supplémentaire.


Qu’est-ce que Span<T> ?

Span<T> est une structure basée sur la pile (stack) qui fonctionne selon le principe de tranche (slice) en mémoire. Elle maintient une référence à une portion d’un tableau, d’une chaîne ou d’une mémoire non gérée, sans effectuer de copie. Cependant, comme elle réside sur la pile, elle ne peut pas être utilisée dans des méthodes async ni être transférée sur le tas (heap).


using System;

class Program
{
    static void Main()
    {
        int[] nombres = { 10, 20, 30, 40, 50 };

        Span<int> tranche = nombres.AsSpan(1, 3); // 20,30,40
        tranche[0] = 99;

        Console.WriteLine(string.Join(", ", nombres)); 
        // Sortie : 10, 99, 30, 40, 50 — car il pointe vers la même mémoire
    }
}

Accès sans copie grâce à Slice

Le principal avantage de Span<T> est d’accéder à des sous-sections de données sans les copier. Par exemple, lorsque vous souhaitez travailler sur une partie spécifique d’un grand tableau d’octets, il n’est pas nécessaire d’en créer un nouveau.


byte[] tampon = new byte[1000];
Span<byte> entete = tampon.AsSpan(0, 128); // section d’en-tête
Span<byte> donnees = tampon.AsSpan(128);    // le reste des données

// Différentes sections du même tableau sans duplication en mémoire
entete.Clear(); // efface uniquement les 128 premiers octets

stackalloc : Mémoire temporaire sans allocation sur le tas

L’expression stackalloc permet d’allouer une mémoire temporaire sur la pile au lieu du tas. Cette mémoire est automatiquement libérée et n’est pas suivie par le ramasse-miettes (GC). Elle est très efficace pour les scénarios de traitement de petites données à haute fréquence.


Span<int> nombres = stackalloc int[5];
for (int i = 0; i < nombres.Length; i++)
    nombres[i] = i * 10;

Console.WriteLine(string.Join(", ", nombres.ToArray()));

Remarque : stackalloc ne fonctionne qu’avec Span<T> et doit être utilisée pour de petites tailles. Un dépassement de pile (stack overflow) peut se produire pour de gros volumes de données.


Qu’est-ce que Memory<T> ?

Memory<T> est la version basée sur le tas et sécurisée pour l’asynchrone de Span<T>. Alors que Span<T> ne vit que sur la pile, Memory<T> peut être stockée sur le tas et utilisée en toute sécurité dans des scénarios asynchrones tels que async/await.


using System;

class Program
{
    static async Task Main()
    {
        byte[] donnees = new byte[1024];
        Memory<byte> memoire = donnees.AsMemory();

        await TraiterAsync(memoire.Slice(100, 200));
    }

    static async Task TraiterAsync(Memory<byte> data)
    {
        await Task.Delay(100); // simulation de traitement asynchrone
        var span = data.Span;
        span.Fill(255); // accès direct à la mémoire
        Console.WriteLine("Segment rempli.");
    }
}

Vous pouvez accéder directement à Span<T> via la propriété Span de Memory<T>.


ReadOnlySpan<T> et ReadOnlyMemory<T>

Si vous souhaitez que les données soient en lecture seule, utilisez ReadOnlySpan<T> ou ReadOnlyMemory<T>. Elles permettent un accès sans copie tout en empêchant toute modification.


ReadOnlySpan<char> span = "Bonjour le monde".AsSpan();
Console.WriteLine(span.Slice(8)); // le monde

Traitement des chaînes avec Span

En utilisant Span<char>, vous pouvez effectuer des opérations de découpage sur les chaînes beaucoup plus rapidement qu’avec Substring(). Aucune nouvelle chaîne n’est créée — on accède simplement à une portion de la chaîne d’origine.


ReadOnlySpan<char> texte = "1234-5678-9012-3456";
ReadOnlySpan<char> derniers4 = texte.Slice(texte.Length - 4);

Console.WriteLine($"Les 4 derniers chiffres : {derniers4.ToString()}");

Comparaison Span vs ArraySegment

ArraySegment<T> représente une portion d’un tableau, mais ne fonctionne qu’avec les tableaux. Span<T>, en revanche, peut travailler avec différents types de sources de données, comme la mémoire, les chaînes ou les pointeurs. De plus, Span<T> étant une ref struct, il réduit la pression sur le GC.


int[] nombres = { 1, 2, 3, 4, 5 };
var segment = new ArraySegment<int>(nombres, 1, 3);
Span<int> tranche = nombres.AsSpan(1, 3);

Comparaison de performance

L’exemple suivant montre la différence entre le découpage classique de chaîne et l’utilisation de Span<T>. Substring() crée un nouvel objet chaîne, tandis que AsSpan() se contente de référencer la mémoire existante.


string texte = "Test de performance CSharp";

var partie1 = texte.Substring(7, 10);           // crée une nouvelle chaîne
ReadOnlySpan<char> partie2 = texte.AsSpan(7, 10); // aucune copie

Console.WriteLine(partie1);
Console.WriteLine(partie2.ToString());

En particulier pour le traitement de chaînes ou de données binaires volumineuses, Span réduit considérablement la pression sur le GC.


Points importants à considérer lors de l’utilisation de Span et Memory


Exemple : Découper un tableau d’octets

Par exemple, lors de l’analyse d’un paquet réseau ou de la lecture d’en-têtes de fichier, traiter de grands tableaux d’octets sans copie améliore considérablement les performances. L’exemple suivant divise un paquet de données en en-tête et corps puis les traite séparément.


using System;

class Program
{
    static void Main()
    {
        byte[] paquet = new byte[1024];
        new Random().NextBytes(paquet);

        Span<byte> span = paquet.AsSpan();

        Span<byte> entete = span.Slice(0, 128);
        Span<byte> corps = span.Slice(128);

        Console.WriteLine($"Longueur de l’en-tête : {entete.Length}");
        Console.WriteLine($"Longueur du corps : {corps.Length}");
    }
}

TL;DR

  • Span<T> : segment de mémoire rapide, sans copie, basé sur la pile. (Courte durée de vie, non compatible async.)
  • Memory<T> : basé sur le tas, compatible async, référence mémoire sûre.
  • ReadOnlySpan / ReadOnlyMemory : accès en lecture seule, préserve l’intégrité des données.
  • stackalloc : élimine la pression du GC pour les petites données temporaires.
  • Traitez de grandes données sans copie pour réduire considérablement la charge CPU et GC.

Articles connexes