Amélioration de la chaine interpolée avec .Net 6 et C# 10 

L’arrivée de C# 10 et de .net 6 apporte un changement important sur les chaines interpolées. Je vais dans cet article vous détailler ce changement qui, s’il ne change rien sur la forme, change considérablement sur le fond.  

Benchmark

Pour commencer, il s’agit avant tout de rappeler ce qu’est une chaine interpolée. Il s’agit de la possibilité de concaténer un string en utilisant l’écriture $"{variable1}du texte{variable2}";.

Entre C# 9 et C# 10, l’écriture reste la même. Cependant l’exécution diffère.

Afin d’illustrer mes propos, j’ai réalisé un petit programme me permettant de faire un benchmark sur différente façon de concaténer une chaine de caractères.

J’ai donc comparé différentes méthodes permettant d’afficher une date. Six méthodes permettant d’arriver au même résultat.

Les différents benchmarks

J’ai d’abord exécuté le programme en .Net 5 et C#9 puis .Net 6 et C# 10.

Résultats du benchmark .Net 5

Avec .net 5, on constate déjà quelques différences.

L’utilisation de Stringbuilder est la méthode la plus rapide, mais pas la moins consommatrice de mémoire.

Enfin l’utilisation de la chaine interpolée est la moins consommatrice de mémoire, mais c’est également l’une des plus lentes.

Résultats du benchmark .Net 6

Avec .net 6, on constate d’emblée une nette amélioration des performances au niveau de la vitesse d’exécution pour le stringbuilder et la chaine interpolée.

La consommation mémoire reste sensiblement la même, hormis pour la chaine interpolée qui voit sa consommation drastiquement réduite.

Une simple comparaison entre ces deux tableaux met vraiment en exergue le gain significatif de performance, tant en vitesse qu’en mémoire pour la chaine interpolée.

Pourquoi une telle différence ? C’est ce qu’on va voir maintenant.

Changement de conception

Pour comprendre le résultat, il faut regarder ce qui se passe quand on utilise la chaine interpolée.

En .Net 5, on peut voir que la méthode fait simplement appel à une autre méthode : string.Format.

public static string Format([NotNull] string format, object? arg0, object? arg1, object? arg2)

Cette méthode comporte 3 arguments qui sont les nombres renseignés (1993, 6 et 15).

Ainsi la chaine interpolée est convertie exactement en string.Format(« {0}/{1}/{2} », 1993, 6, 15).

En quoi est-ce un problème ? Le souci vient du fait que les arguments sont des objets. Quand vous envoyez un integer, qui est un valueType, il devrait aller directement dans la pile, mais il doit d’abord aller dans un objet ce qui est une allocation de tas. Cette dernière allocation coute cher et explique les résultats observés.

C’est ce point qui est modifié avec .net 6. 

C# 10 utilise maintenant DefaultInterpolatedStringHandler.

Pour comprendre ce que ça change, je vais détailler ce dessus le code équivalent pour chaque méthode.

return string.Concat(1993, "/", 6, "/", 15);

Renvoie :

object[] array = new object[5];
    array[0] = 1993;
    array[1] = "/";
    array[2] = 6;
    array[3] = "/";
    array[4] = 9
    Return string.Concat(array);

String.Concat produit un tableau d’objet et chaque élément de la chaine et placé dans une case de ce tableau. Cela signifie que chaque élément, et notamment les integer, est placé dans un objet. Tout part dans le tas et ceci explique les données du tableau avec les faibles performances.

return string.Concat(1993.ToString(), "/", 6.ToString(), "/", 15.ToString());

Équivaut à

string[] array = new string[5];
    array[0] = 1993.ToString();
    array[1] = "/";
    array[2] = 6.ToString();
    array[3] = "/";
    array[4] = 9.ToString()
    return string.Concat(array);

La différence marquante est le changement d’un tableau d’objet à un tableau de string. L’utilisation du ToString() permet au compilateur de détecter qu’on ne concatène que des string. Il n’y a plus besoin de placer certains éléments dans des objets et cela est plus efficace.

return 1993 + "/" + 6 + "/" + 15;

Équivaut à

string[] array = new string[5];
    array[0] = 1993.ToString();
    array[1] = "/";
    array[2] = 6.ToString();
    array[3] = "/";
    array[4] = 9.ToString()
    return string.Concat(array);

Le retour est donc exactement le même que précédemment avec les mêmes conclusions.

Le stringbuilder et le stringformat restent les mêmes.

On arrive maintenant à la partie qui nous intéresse. La chaine interpolée se comporte donc différemment en fonction de la version de C# et de .NET.

En .Net 5, comme nous l’avons vu un peu plus haut, la chaine devient simplement un string.format :

string.Format("{0}/{1}/{2}", 1993, 6, 15);

Regardons maintenant ce qu’il en est avec .Net 6.

return $"{1993}/{6}/{9}";

Équivaut à

DefaultInterpolatedStringHandler handler = new DefaultInterpolatedStringHandler(literalLength: 2, formattedCount: 3);
    handler.AppendFormatted(1993);
    handler.AppendLiteral("/");
    handler.AppendFormatted(6);
    handler.AppendLiteral("/");
    handler.AppendFormatted(15);
    return handler.ToStringAndClear();

Cela n’a plus rien à voir. La méthode utilise à présent un DefaultInterpolatedStringHandler avec des composants Formatted et Literal. 

Pourquoi est-ce plus efficace me direz-vous ?

Pour y voir plus clair, il faut regarder plus en détail les méthodes  AppendFormatted et AppendLiteral.

AppendLiteral est appelée pour ajouter un string dans le gestionnaire de string. Peu de chose à dire si ce n’est qu’elle le fait bien.

Le plus intéressant ici est l’utilisation de AppendFormatted. Cette méthode utilise les génériques :

public void AppendFormatted<T>(T value);

L’utilisation des génériques est importante parce qu’il permet de formater T non pas dans un string temporaire, mais directement dans la mémoire tampon du gestionnaire.

Dans notre cas, pour 1993, la méthode appelée sera AppendFormatted<int32>.

 La chaine interpolée sera donc analysée afin d’extraire chaque partie pour appeler la méthode la plus adaptée au type.

C’est ce mécanisme qui permet d’améliorer considérablement l’allocation mémoire nécessaire pour gérer la chaine interpolée.

Conclusion

Comme on peut le voir dans cet article, .Net 6 et C# 10 apportent de vraies solutions sur les performances. Il devient maintenant plus efficace d’utiliser la chaine interpolée lorsque l’on veut faire des économies sur la mémoire sans pour autant sacrifier les performances. Et il ne s’agit là que d’un exemple parmi les nombreuses améliorations apportées par la combinaison .Net 6 et C# 10.

Laisser un commentaire