À la découverte du nouveau type «Span» introduit dans C# 7.2

Un petit nouveau fait son entrée dans la liste des types supportés par le Framework .NET, Span<T> !
Ce nouveau type a principalement été créé dans le but de résoudre des problématiques de performances, ceci grâce à une meilleure gestion des pointeurs et un accès aux variables en mémoire simplifié.

/!\ Cet article se base sur la dernière version de dot net core 2.1 preview 1 :

Announcing .NET Core 2.1 Preview 1

Nous allons voir dans cet article à quelle problématique ce nouveau type va pouvoir répondre et les nouveaux cas d’usages possibles avec celui-ci.

Certaines fonctionnalités sont susceptibles d’être revues lors de la sortie finale de la version.

Hey Doc, c’est quoi le souci ?

Pour comprendre la raison pour laquelle Span<T> a été créé, il faut d’abord comprendre les différentes mémoires avec lesquelles on peut travailler dans le monde .NET.

Il y a 3 types de gestion de mémoire possible :

La « Managed Memory »

C’est la mémoire utilisée lorsque l’on crée une nouvelle instance via le mot clé « new ». Elle va contenir toutes les variables par références et est gérée par le Garbage Collector de façon automatique.
Il est difficile d’évaluer sa durée de vie, sa libération de mémoire étant faite de façon automatique.
En effet, le Garbage Collector a en interne un système de nettoyage des objets en mémoire, à intervalles réguliers il va parcourir son graphe de références, et si une référence n’est plus utilisée, celle-ci sera détruite et libérée de la mémoire.

La « Stack Memory »

On peut y accéder grâce à la commande « stackalloc » et permet une allocation et désallocation rapide en mémoire d’une variable de type « value » dans la stack.
Sa durée de vie se limite au scope de son exécution (par exemple à la fin de l’exécution d’une fonction).
Exemple d’utilisation:

unsafe public void StackExemple()
{
    char* helloString = stackallock char [5];
}

À la fin de cette fonction, la variable helloString qui est allouée dans la stack va être automatiquement libérée de la mémoire.

La « Unmanaged memory »

Comme son nom l’indique, c’est une mémoire qui n’est pas contrôlée par le Garbage Collector et est donc à utiliser avec grande précaution, car la libération de cette mémoire est à la responsabilité du développeur.
Elle est à privilégier lorsque l’on veut alléger le travail du Garbage Collector, voire obligatoire si l’on veut faire de l’interopérabilité. Utile quand on doit travailler avec des gros blocs de données !

On peut interagir avec grâce à la classe Marshal.
(cf. https://msdn.microsoft.com/fr-fr/library/system.runtime.interopservices.marshal(v=vs.110).aspx)

Mais pourquoi je vous parle de ces 3 mémoires là ?

Tout simplement car c’est un prérequis pour comprendre la problématique que résout Span<T>.
Pour cela, je vais vous exposer un cas concret qui pointe du doigt le souci.
Imaginons que l’on veuille mettre en place une méthode prenant en paramètre une string et qui retourne cette même string inversée.
« Facile », me direz-vous, on fait une méthode ayant comme signature :

string ReverseString (string parameterString);

Mais là, on ne gère que les variables managées automatiquement par le GC (la stack et la heap). Si quelqu’un veut utiliser une variable en mémoire non managée par le GC, il faudra implémenter également une autre signature prenant comme paramètre un pointeur :

unsafe String ReverseString(char* parameterStringUnmanaged, int lenght);

Supposons maintenant que l’on veuille gérer des portions de texte.
On ajoute 2 signatures, l’une pour les mémoires managées et l’autre acceptant un pointeur :

string ReverseString(string parameterString, int startIndex, int lenght);

unsafe string ReverseString(char* parameterStringUnmanaged, int lenght, int startIndex, int endIndex);

On se rend compte que pour chaque méthode, il faudra un équivalent pour prendre en compte les variables en mémoire non managées !
Cela va multiplier notre code et en rendre la maintenance plus compliquée !

Span<T> à notre rescousse

Eh bien vous savez quoi ? Le nouveau type Span<T> gère automatiquement l’accès aux 3 mémoires du monde .NET sans se soucier du type !
Cela nous garantit un accès et un typage sécurisés à l’utilisation.

Span<T> dans la pratique

Nous pouvons créer un Span<T> pour de la mémoire non managée de cette façon :

unsafe
{
IntPtr unmanagedPointer = Marshal.AllocHGlobal(128);
Span<byte> unmanagedData = new Span<byte>( unmanagedPointer.ToPointer(), 128);
Marshal.FreeHGlobal(unmanagedPointer);
}

Si nous avons un Array de char, nous pouvons le caster implicitement vers un Span :

char[] array = new char[] { 'S', 'O', 'A', 'T'};
Span<char> fromArray = array;

Enfin, lorsque nous devons travailler sur des strings ou autres types immuables, nous pouvons utiliser ReadOnlySpan<T> :

string stringSpan = "hello joel";
ReadOnlySpan<char> fromString = stringSpan.AsReadOnlySpan;

Refactorisation de notre code

Grâce à Span<T> nous pouvons maintenant factoriser nos méthodes :

string ReverseString(string parameterString, int startIndex, int lenght);

unsafe string ReverseString(char* parameterStringUnmanaged, int lenght, int startIndex, int endIndex);

Ces 2 signatures peuvent maintenant être fusionnées en une seule et unique méthode qui implémentera la logique :

public string ReverseStringTest(ReadOnlySpan<char> parameterString) ;

Nous avons grâce à Span réussi à travailler avec 2 types de mémoire différents, ceci de façon simple, sécurisée et avec une seule et même méthode !

Quels sont les autres avantages ?

L’un des gros avantages de Span<T> est sa capacité à splitter des données en plusieurs morceaux sans que cela coûte de la mémoire système.
Comme tout à l’heure, on va prendre un cas concret pour en démontrer l’intérêt. Analysons donc le code suivant :

string grosVolumeDeDonnees = "Beaucoup de données ici";

string boutDeDonnee_AvecAllocation = grosVolumeDeDonnees.Substring(1, 5);

Ici, nous déclarons une string avec beaucoup de données, ce qui va engendrer déjà un coût en mémoire dans la stack.

Ensuite, nous voulons récupérer un bout de cette string en faisant un substring… En faisant cela, une autre allocation en mémoire va être effectuée, ce qui va augmenter drastiquement le coût de nos opérations.

Avec ReadOnlySpan, aucune allocation en mémoire n’est effectuée, seuls les pointeurs vont être déplacés pour correspondre au morceau de texte que nous voulons !

ReadOnlySpan<char> boutDeDonnee_SansAllocation = grosVolumeDeDonnees.AsReadOnlySpan().Slice(1, 5);

Des tests ont été effectués pour comparer les coûts et vitesses d’exécution entre le substring et le slice d’un Span, et les résultats sont sans appel !

On peut imaginer de multiples usages :
– Parcourir des grosses chaînes de caractères sans allocation mémoire
– Parser du json/xml
– Lire et écrire des données binaires
– Etc…

Limitations de Span<T>

Comme Span est censé pouvoir gérer toutes les mémoires de notre application, des limitations d’usages sont imposées pour des raisons techniques.

Stack Only

Les Span ne peuvent être stockés eux-mêmes que dans la mémoire stack. Ce choix a été motivé par plusieurs facteurs, mais les principaux sont dus à la volonté de simplifier le travail du Garbage Collector.

Pour plus de détails, vous pouvez consulter ce chapitre qui explique et motive ce choix :
https://github.com/dotnet/corefxlab/blob/master/docs/specs/span.md

De cette limitation en découlent d’autres, implicites :

Etant donné que Span ne peut résider que dans la stack :

  • Span ne peut pas être une propriété d’une classe (cette dernière étant stockée dans le heap)
  • Span ne peut être boxé

Ces limitations diminuent le scope des possibles usages, mais l’équipe de dev est en train de travailler à des solutions pour aller plus loin, notamment grâce à Memory<T>.

Ce type permettra de travailler dans la mémoire heap, mais est aujourd’hui toujours en pleine implémentation et va encore énormément changer, c’est pour cela que je ne m’attarde pas sur lui dans cet article.

Je vous renvoie vers la documentation officielle, disponible à cette adresse :
https://github.com/dotnet/corefxlab/blob/master/docs/specs/memory.md

En résumé et en conclusion

Span<T> se montre extrêmement prometteur pour travailler avec de gros volumes de données, ou bien pour se simplifier les accès aux différentes mémoires de l’application.
On sent tout de même que l’on est encore qu’au début de l’implémentation, il reste beaucoup d’axes d’améliorations. Il faudra donc attendre encore quelques versions pour pouvoir l’utiliser de façon pérenne et efficace en production.
Ce nouveau type risque bien de devenir indispensable dès lors que l’on voudra optimiser nos temps de calculs.

© SOAT
Toute reproduction interdite sans autorisation de l’auteur

Nombre de vue : 725

COMMENTAIRES 3 commentaires

  1. Julien VINCENT dit :

    Merci pour cette explication très claire

  2. Rémi Lamotte dit :

    N’y aurait-il pas un problème dans le tableau d’analyse des vitesses et coût d’exécution? Il semble que selon le tableau le traitement de 10 caractères soit plus lent que celui de 100 caractères…

  3. Joel PINTO RIBEIRO dit :

    @Rémi Lamotte :

    C’est une très bonne remarque , enfaîte on se rend surtout compte que faire un slice sur un Span a un coût d’exécution constant. Du coup la durée n’est pas dépendant de la longueur de caractères , et dans notre exemple le faire sur 10 caractères, selon un certain contexte d’exécution peut effectivement être (très) légèrement plus lent. En rejouant ce test 10 fois sur 10/1000/10000 caractères on se rendra compte qu’on aura quasiment tout le temps le même temps d’exécution.

AJOUTER UN COMMENTAIRE