Un traitement lit un buffer, extrait quelques segments, transforme les données puis recommence plusieurs milliers de fois.
Le code semble simple. Pourtant, le profiler montre une pression GC importante.
Le problème vient souvent des copies intermédiaires.
var payload = buffer .Skip(offset) .Take(length) .ToArray();
À chaque passage, un nouveau tableau est alloué.
Sur un traitement ponctuel, cela ne change rien. Dans une hot path, ces allocations finissent par peser sur le Garbage Collector.
Travailler sur une vue avec Span
Span<T> permet de travailler sur une région contiguë de mémoire sans recopier les données.
byte[] buffer = GetBuffer();Span<byte> payload = buffer.AsSpan( offset, length);Process(payload);
payload représente ici une vue sur le tableau existant.
Aucun nouveau buffer contenant les données n’est créé.
Le même principe fonctionne particulièrement bien pour parser une chaîne.
ReadOnlySpan<char> line = "FR001;1250.42;EUR";var separator = line.IndexOf(';');ReadOnlySpan<char> country = line[..separator];
On découpe la mémoire existante plutôt que de produire plusieurs chaînes temporaires.
C’est précisément le type de scénario pour lequel Span<T> est intéressant : parsing, protocoles réseau, traitements de fichiers ou manipulation intensive de buffers. Microsoft le décrit comme une représentation type-safe d’une région contiguë de mémoire pouvant s’appuyer sur un tableau, la stack ou de la mémoire non managée.
Une contrainte importante : sa durée de vie
Span<T> est un ref struct.
Cette propriété impose des contraintes fortes sur sa durée de vie. Un Span<T> ne peut notamment pas être stocké comme champ d’une classe classique ni traverser librement une frontière await.
Le compilateur empêche ce type d’utilisation.
public async Task ProcessAsync( Memory<byte> buffer){ await SaveAsync(buffer); Span<byte> span = buffer.Span; Process(span);}
C’est ici que Memory<T> prend le relais.
Memory<T> représente elle aussi une région de mémoire sans imposer les mêmes contraintes de durée de vie. Elle peut être conservée dans un objet ou passée dans une API asynchrone. Lorsqu’il faut réellement travailler sur les données, on récupère son Span.
Une règle pratique fonctionne bien : Span<T> pour le traitement synchrone local, Memory<T> lorsqu’un buffer doit survivre au scope courant ou traverser une opération asynchrone.
Éviter de recréer les buffers
Il reste un autre type d’allocation fréquent dans les traitements intensifs.
var buffer = new byte[64 * 1024];await stream.ReadAsync(buffer);
Si cette opération est répétée continuellement, l’application crée puis abandonne de nombreux tableaux.
ArrayPool<T> permet de réutiliser ces buffers.
var pool = ArrayPool<byte>.Shared;byte[] buffer = pool.Rent(64 * 1024);try{ var read = await stream.ReadAsync( buffer.AsMemory(0, 64 * 1024)); Process(buffer.AsSpan(0, read));}finally{ pool.Return(buffer);}
Le tableau est loué puis rendu au pool.
Rent garantit une taille au moins égale à celle demandée. Le tableau retourné peut donc être plus grand, et son contenu précédent peut encore être présent.
Ce dernier point compte particulièrement lorsqu’on manipule des données sensibles.
pool.Return( buffer, clearArray: true);
Le buffer peut alors être nettoyé avant d’être réutilisé.
Après un Return, le code ne doit plus utiliser la référence. Le buffer appartient à nouveau au pool et peut être attribué à un autre traitement.
Les trois mécanismes travaillent souvent ensemble
Dans un pipeline réseau ou un traitement de fichier, on retrouve souvent cette organisation :

ArrayPool<T> réduit le nombre de tableaux créés.
Memory<T> transporte le buffer lorsque sa durée de vie dépasse une méthode synchrone.
Span<T> devient la vue de travail utilisée par le code qui parse ou transforme les données.
Ils répondent donc à des problèmes différents au sein du même flux.
Le profiler décide si cela vaut le coût
Introduire Span<T> et du pooling partout rend rapidement le code plus difficile à lire.
Je réserve généralement ces mécanismes aux zones où les allocations apparaissent réellement dans les mesures : parsing intensif, gros volumes, buffers réseau, sérialisation ou traitements exécutés très fréquemment.
Visual Studio Performance Profiler ou BenchmarkDotNet permettent déjà de vérifier le nombre d’allocations et le volume mémoire généré.
Si le profiler ne montre aucune pression GC significative, remplacer un code simple par du pooling apporte rarement quelque chose d’utile.
La bonne optimisation commence ici : supprimer les allocations qui apparaissent réellement dans la mesure.




