C#11: Comment les chaînes littérales UTF-8 vont améliorer vos performances

miniature

Le web fait un usage systématique et abondant de l’UTF-8Cette norme d’encodage du texte est devenue incontournable pour afficher tous types de langues, du français ou de l’anglais, tout comme de l’hindi ou du sanskrit.

Toutes ces langues, sur la même page web. N’est-ce pas là un beau message d’unité, de cohésion ? Sauf que voilà. C#, jusque là, venait y mettre son petit grain de sel.

 

Le hic

Une string littérale est tout simplement une chaîne de caractères avec une valeur définie à la compilation par le développeur, ainsi:

string toto = "ma string littérale"

En C#, toute instance du type string est en UTF-16.

Alors, bien sûr, ASP.NET Core est capable de générer des pages web UTF-8 sans aucun souci, l’UTF-8 faisant partie intégrante du fonctionnement des classes du namespace System.Text.Encoding de .NET.

Cependant, depuis les débuts de C#, toutes les string littérales sont en fait des strings en UTF-16, qu’il faut ensuite convertir au runtime en UTF-8, pour qu’elles puissent être affichées correctement sur un contenu web.

Et en C#, les chaînes de caractère UTF-8 sont représentées par des tableaux de bytes (byte[]).

Je vous donne un exemple:

// Les strings littérales sont en UTF-16.
static readonly string nom = "César";

// Une conversion existe, mais le coût de cette conversion doit être considéré
static readonly byte[] nom_utf8 = Encoding.UTF8.GetBytes(nom);

// La manière la plus optimisée, mais compliquée à écrire
static readonly byte[] nom_utf8_en_dur = new[] { 0x67, 0x195, 0x169, 0x115, 0x97, 0x114 };

Comme énoncé en commentaire de ce code, le coût d’une telle conversion d’encodage doit être considéré. Le start-up cost, ainsi que l’allocation mémoire à cet instant T peuvent-être considérables puisque cette conversion est faite au runtime.

Ici, j’vous l’accorde, le nom "César" est court. Mais imaginez donc un payload de plusieurs milliers de lignes, à convertir pour le retourner en réponse HTTP.

De plus, ASP.NET faisant un usage continu de constantes littérales, ne serait-ce que pour écrire la première ligne d’une réponse HTTP ( "HTTP/1.0"), cela pourrait avoir des conséquences cataclysmiques sur la performance.

La manière la plus optimisée d’écrire ces valeurs est donc d’écrire directement unbyte[]. Chose que les moins barbus d’entre nous ne feront probablement jamais.

Cela est bien dommage, car les  byte[]combinés aux Streams, peuvent grandement améliorer la performance lors de l’écriture récurrente de payloads.

Vous voyez, maintenant, pourquoi je disais que C# est venu mettre son grain de sel? Cette systématisation de la conversion en UTF-8 au runtime vient maintenant causer des problèmes de performance, alors que l’UTF-8 était la panacée unificatrice depuis 1996!

Ce serait quand même vachement bien que les string littérales soient par défaut en UTF-8, non ?

 

La solution de C#11: Les chaînes littérales UTF-8

En C#11, une nouveauté vient changer la donne:

var nom = "César"u8;

Notons le suffixe u8 en fin de chaîne.

Avec cette notation, on peut définir des chaînes de caractère au format UTF-8.
En réalité, il s’agit d’un 
sucre syntaxique bien élaboré, car le compilateur changera le code ci-dessus par:

ReadOnlySpan<byte> nom = new[] { 0x67, 0x195, 0x169, 0x115, 0x97, 0x114 };

Notons ici, le choix du compilateur pour le type ReadOnlySpan<byte>. Le ReadOnlySpan<T>, consiste en un morceau (un chunk) d’un tableau. Ici, c’est donc un chunk d’un byte[].

De plus, le ReadOnlySpan<T> est une ref struct, ce qui signifie que c’est une structure de données qui sera toujours allouée dans la stack.

L’allocation sur la stack, c’est exactement le comportement mémoire que l’on souhaite pour une valeur telle qu’une chaîne de caractères. Pour plus d’infos là dessus, visitez cette doc sur le ReadOnlySpan.

Excellente nouvelle! Le compilateur, via une phase de pré-compilation, a converti la chaîne UTF-8 en sa forme la plus optimisée pour tout traitement.

Ainsi, comme la chaîne de caractère est sous sa forme la plus optimisée, les performances des manipulations permettant de construire une chaîne UTF-8 s’en retrouvent grandement améliorées.

Voyons ici un exemple qui effectue la concaténation entre une chaîne UTF-16 et une chaîne UTF-8:

using System.IO;
using System.Text;
var insider = new Insider()
{
    NomDeFamille = "Mourot"
};

var prenom_utf8 = "César"u8;
var nom_utf16 = insider.NomDeFamille;
var nom_utf8 = Encoding.UTF8.GetBytes(nom_utf16);

using var memoryStream = new MemoryStream();
memoryStream.Write(prenom_utf8);
memoryStream.Write(" "u8);
memoryStream.Write(nom_utf8);
memoryStream.Position = 0;

using var reader = new StreamReader(memoryStream);
Console.WriteLine(reader.ReadToEnd());

Le code parle de lui-même. On transforme le nom de famille "Mourot" en une chaîne UTF-8.

Puis, on vient écrire le nom et le prénom (déjà en UTF-8) dans un MemoryStream, qui est récupéré par un StreamReader, dont le contenu est finalement affiché dans la console.

Voici donc ce à quoi ressemble l’output de ce code:L’avantage réside donc ici dans le fait que la chaîne "César"u8 était déjà en UTF-8. La conversion vers l’UTF-8 n’était ainsi pas nécessaire pour cette chaîne, et nous avons donc converti uniquement la chaîne qui n’était pas déjà en UTF-8 ("Mourot").

Nous sauvons ainsi quelques précieuses secondes de Cloud computing, car faire toutes les manipulations en UTF-16, puis tout convertir en UTF-8 induit un risque de diminuer les performances.

Magnifique ! Peut-être un peu too much pour une concaténation de chaîne, mais pourtant nécessaire pour gérer deux encodings tout en améliorant les performances.

 

Conclusion

C#11, avec les chaînes UTF-8 littérales, améliore grandement les performances des manipulations de chaînes de caractère UTF-8.

ASP.NET, ainsi que la couche réseau des librairies .NET, vont s’emparer de cette nouveauté et ainsi améliorer grandement la vélocité des applications web des développeurs .NET.
Cela reste tout de même une optimisation qui n’est 
pas à la portée de tous les développeurs. Le type ReadOnlySpan n’ayant pas la même flexibilité que le type String, il y a peu de chances que cette nouveauté soit adoptée par madame ou monsieur tout le monde.

Ce sera ainsi une amélioration fabuleuse qui bénéficiera under the hood à tous les développeurs .NET.

Christopher Jamme de Lagoutine

Software Craftsman à Code Insider, Christopher a acquis au fil de son expérience de solides bases en ingénierie web, en intégration continue et en architecture cloud. Durant son temps libre, il aime développer des applications natives.

Vous aimerez aussi...

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.