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
Span<T>réside sur la pile ; il ne peut pas être utilisé dans les méthodes asynchrones ni en dehors des lambdas.Memory<T>réside sur le tas ; il doit être privilégié dans les scénarios asynchrones.- La durée de vie des données sous-jacentes doit être supérieure à celle du
Span(sinon la mémoire peut être corrompue). stackallocdoit être utilisé uniquement pour de petites données (par exemple < 1 Ko).- Très efficace pour le texte, le réseau, le JSON ou le traitement de données binaires à haute performance.
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
Code non sécurisé et pointeurs en C#
Apprenez le code non sécurisé et les pointeurs en C# pour manipuler la mémoire et les opérations bas niveau.
Génériques en C# (List<T>, Dictionary<TKey,TValue>)
Apprenez les génériques en C# (List<T>, Dictionary<TKey,TValue>) pour écrire un code réutilisable et typé, avec exemples.
Gestion de la mémoire et Garbage Collector en C#
Apprenez la gestion de la mémoire et le garbage collector en C# pour comprendre le cycle de vie des objets.
Le concept des générateurs de code en C# (C# 9+)
Découvrez les générateurs de code en C# pour générer du code à la compilation et améliorer les performances.
Structs en C# – Différences avec les classes
Découvrez les différences entre structs et classes en C#, notamment le modèle mémoire, l’héritage et les performances.
Tableaux (Arrays) en C#
Apprenez les tableaux en C# : déclaration, indexation, parcours avec boucles et opérations courantes avec exemples.
Types de données de base en C#
Types de données de base en C# : numériques, textuels, logiques, orientés objet et nullables.