Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Le Mythe du StringBuilder

Sur l'excellent blog du non moins excellent Mitsu, dans l'une de mes interventions sur l'un de ses (excellents aussi) petits quizz LINQ, j'aurai parlé du "Mythe du StringBuilder". Cela semble avoir choqué certains lecteurs qui m'en ont fait part directement. Le sujet est très intéressant et loin de toute polémique j'en profiterais donc ici pour préciser le fond de cet avis sur le StringBuilder et aussi corriger ce qui semble être une erreur de lecture (trop rapide?) de la part de ces lecteurs. J'ai même reçu un petit topo sur les avantages de StringBuilder (un bench intéressant par ailleurs).

D'abord, plantons le décor

Le billet de Mitsu dans lequel je suis intervenu se trouve ici. Dans cet échange nous parlions en fait de la gestion des Exceptions que l'un des intervenants disait ne pas apprécier en raison de leur impact négatif en terme de performance. Et c'est là que j'ai répondu :

"...Dans la réalité je n'ai jamais vu une application (java, delphi ou C#) "ramer" à cause d'une gestion d'exception, c'est un mythe à mon sens du même ordre que celui qui veut que sous .NET il faut systématiquement utiliser un StringBuilder pour faire une concaténation de chaînes."

Comme on le voit ici, seule une lecture un peu trop rapide a pu faire croire à certains lecteurs que je taxe les bénéfices du StringBuilder de "mythe". Je pensais que la phrase était assez claire et qu'il était évident que si mythe il y a, c'est dans le bénéfice systématique du StringBuilder... Nuance. Grosse nuance.

Mythe ou pas ?

La gestion des exceptions est un outil fantastique "légèrement" plus sophistiqué que le "ON ERROR GOTO" de mon premier Basic Microsoft interprété sous CP/M il y a bien longtemps, mais le principe est le même. Et si en 25 ans d'évolution, la sélection darwinienne a conservé le principe malgré le bond technologique c'est que c'est utile...

Est-ce coûteux ?

Je répondrais que la question n'a pas de sens... Car quel est le référenciel ?

Les développeurs ont toujours tendance à se focaliser sur le code, voire à s'enfermer dans les comparaisons en millisecondes et en octets oubliant que leur code s'exprime dans un ensemble plus vaste qui est une application complète et que celle-ci, pour être professionnelle a des impératifs très éloignés de ce genre de débat très techniques. Notamment, une application professionnelle se doit avant tout de répondre à 3 critères : Répondre au cachier des charges, avoir un code lisible, être maintenable. De fait, si les performances ne doivent pas être négligées pour autant, le "coût" d'une syntaxe, de l'emploi d'un code ou d'un autre, doit être "pesé" à l'aulne d'un ensemble de critères où celui de la pure performance ne joue qu'un rôle accessoire dans la grande majorité des applications. Non pas que les performances ne comptent pas, mais plutôt que face à certaines "optimisations" plus ou moins obscures, on préfèrera toujours un code moins optimisé s'il est plus maintenable et plus lisible.

Dès lors, le "coût" de l'utilisation des exceptions doit être évalué au regard de l'ensemble de ces critères où les millisecondes ne sont pas l'argument essentiel.

De la bonne mesure

Tout est affaire de bonne mesure dans la vie et cela reste vrai en informatique.

Si une portion de code effectue une boucle de plusieurs milliers de passages, et si ce traitement doit absolument être optimisé à la milliseconde près, alors, en effet, il sera préférable d'éviter une gestion d'exception au profit d'un ou deux tests supplémentaires (tester une valeur à zéro avant de s'en servir pour diviser au lieu de mettre la division dans un bloc Try/Catch par exemple).

Mais comme on le voit ici, il y plusieurs "si" à tout cela. Et comment juger dans l'absolu si on se trouve dans le "bon cas" ou le "mauvais cas" ? Loin de la zone frontière, aux extrèmes, il est toujours facile de décider : une boucle de dix millions de passages avec une division ira plus vite avec un test sur le diviseur qu'avec un Try/Catch (si l'exception est souvent lancée, encore un gros "si" !). De même, un Try/Catch pontuel sur un chargement de document sur lequel l'utilisateur va ensuite travailler de longues minutes n'aura absolument aucun impact sur les performances globales du logiciel.

Mais lorsqu'on approche de la "frontière", c'est là que commence la polémique, chacun pouvant argumenter en se plaçant du point de vue performance ou du point de vue qualité globale du logiciel. Et souvent chacun aura raison sans arriver à le faire admettre à l'autre, bien entendu (un informaticien ne change pas d'avis comme ça :-) ) !

Où est alors la "bonne mesure" ? ... La meilleure mesure dans l'existence c'est vous et votre intelligence. Votre libre arbitre. Il n'y a donc aucun "dogme" qui puisse tenir ni aucune "pensée unique" à laquelle vous devez vous plier. A vous de juger, selon chaque cas particulier si une gestion d'exception est "coûteuse" ou non. N'écoutez pas ceux qui voudraient que vous en mettiez partout, mais n'écoutez pas non plus ceux qui les diabolisent !

Et le StringBuilder dans tout ça ?

Si dans mon intervention sur le blog de Mitsu j'associais gestion des exceptions et StringBuilder c'est parce qu'on peut en dire exactement les mêmes choses !

Dans les cas extrêmes de grosses boucles il est fort simple de voir que le StringBuilder est bien plus performant qu'une concaténation de chaînes avec "+". Cela ne se discute même pas.

Mais, comme pour la gestion des exceptions, c'est lorsqu'on arrive aux "frontières" que la polémique pointe son nez. Pour concaténer quelques chaînes le "+" est toujours une solution acceptable car le StringBuilder a un coût, celui de son instanciation et du code qu'il faut écrire pour le gérer. Il s'agit là des cas les plus fréquents. On concatène bien plus souvent quelques chaines de caractères dans un soft qu'on écrit des boucles pour en concaténer 1 million le tout dans un traitement time critical... (encore des tas de "si" !).

Même du point de vue de la mémoire les choses ne sont pas si simple. Le StringBuilder utilise un buffer qu'il double quand sa capacité est dépassée. Dans certains cas courants (petites boucles dans lesquelles il y a quelques concaténation), le stress mémoire infligé par le StringBuilder peut être très supérieur à celui de l'utilisation de "+" ou de String.Concat.

C'est dans ces cas les plus fréquents que l'utilisation du StringBuilder comme panacée apparaît n'être qu'un mythe.

Conclusion 

Vous et moi écrivons du code, ce code se destine à un client / utilisateur. La première exigence de ce dernier est que le logiciel réponde au cahier des charges et qu'il fonctionne sans bug gênant. Cela impose de votre côté que vous utilisiez une "stylistique" rendant le code lisible et facilement maintenable car un code sans bug n'existe pas et qu'il faudra tôt ou tard intervenir ici ou là. Dans un tel contexte réaliste, ce ne sont pas les millisecondes qui comptent ni les octets, mais bien la qualité globale de l'application. Et c'est alors que les dogmes techniques tombent. Car ils n'ont d'existence que dans un idéal purement technique qui fait abstraction de la réalité. Un Try/Catch ou un StringBuilder ne peuvent pas être étudiés en tant que tels en oubliant les conditions plus vastes de l'application où ils apparaissent. Leur impact n'a de sens que compris comme l'une des milliers d'autres lignes de code d'une application qui doit répondre à un autre critère, celui de la qualité et de la maintenabilité.

Un seul juge existe pour trancher car chaque cas est particulier : vous.

Pour ceux qui veulent jouer un peu avec les StringBuilder, voici un mini projet que vous n'aurez qu'à modifier pour changer les conditions de tests : ConsoleApplication2.zip (6,10 kb)

Mettre des données en forme en une requête LINQ

Losqu'on traite de LINQ, la majorité des exemples utilisent des sources de données "bien formées" : flux RSS (mon dernier billet), collections d'objets, etc. Mais dans la "vraie vie" les sources de données à traiter sont parfois moins "lisses", moins bien formées. LINQ peut aussi apporter son aide dans de tels cas pour transformer des données brutes en collection d'objets ayant un sens. C'est ce que nous allons voir dans ce billet.

Prenons un exemple simple : imaginons que nous récupérons une liste de rendez-vous depuis une autre application, cette liste est constituée de champs qui se suivent, les initiales de la personne, son nom et l'heure du rendez-vous. Tous les rendez-vous se suivent en une longue liste de ces trois champs mais sans aucune notion de groupe ou d'objet.

Pour simplifier l'exemple, fixons une telle liste de rendez-vous dans un tableau de chaînes :

string[] data = new[] { "OD", "Dahan", "18:00", "MF", "Furuta", "12:00", "BG", "Gates", "10:00" };

La question est alors comment extraire de cette liste "linéaire" les trois objets représentant les rendez-vous ?

La réponse passe par trois astuces :

  • L'utilisation de la méthode Select de LINQ qui sait retourner l'index de l'entité traitée
  • La syntaxe très souple de LINQ permettant d'utiliser des expressions LINQ dans les valeurs retournées
  • Et bien entendu la possibilité de créer des types anonymes

Ce qui donne la requête suivante :

var personnes = data.Select ( (s, i) => new
                                          {
                                             Value = s,
                                             Bloc = i / 3
                                          }
                                        ).GroupBy(x => x.Bloc)
                                        .Select ( g => new 
                                                    {
                                                      Initiales = g.First().Value,
                                                      Nom = g.Skip(1).First().Value,
                                                      RendezVous = g.Skip(2).First().Value
                                                     } );

L'énumération suivante : 

foreach (var p in personnes.OrderBy(p=>p.Nom)) Console.WriteLine(p);

donnera alors cette sortie console de toutes les personnes ayant un rendez-vous, classées par ordre alphabétique de leur nom :

{ Initiales = OD, Nom = Dahan, RendezVous = 18:00 }
{ Initiales = MF, Nom = Furuta, RendezVous = 12:00 }
{ Initiales = BG, Nom = Gates, RendezVous = 10:00 }

Voici des objets bien formés (on pourrait ajouter un DateTime.Parse à la création du champ RendezVous pour récupérer une heure plutôt qu'une chaîne) qui pourront être utilisés pour des traitements, des affichages, une exportation, etc...

LINQ to Object ajoute une telle puissance à C# que savoir s'en servir au quotidien pour résoudre tous les petits problèmes de développement qui se posent permet réellement d'augmenter la productivité, ce que tous les langages et IDE promettent falacieusement (aucune mesure de ce supposé gain n'existe). Ici, essayez d'écrire le même code sans LINQ, vous verrez tout suite que le gain de productivité et de fiabilité est bien réel, et que la maintenance aura forcément un coup moindre.

Pour d'autres infos, Stay Tuned !

Le projet VS 2008 : LinqChunk.zip (5,35 kb)

Traiter un flux RSS en 2 lignes ou "les trésors cachés de .NET 3.5"

.NET 3.5 apporte beaucoup de classes nouvelles et d'améliorations à l'existant. Certains ajouts sont plus médiatisés que d'autres. Mais il serait injuste de limiter cette mouture à LINQ ou aux arbres d'expressions, aussi géniales et puissantes soient ces avancées.

.NET 3.5 apporte réellement une foule de nouveautés parmi lesquelles il faut noter :

  • L'apport de WCF et de LINQ au compact framework
  • De nouvelles facilités pour contrôler le Garbarge Collector comme le LatencyMode de la classe GCSettings
  • L'ajout de l'assemblage System.NetPeerToPeer.Collaboration qui permet d'utiliser les infrastructures de peer-to-peer
  • Des améliorations importantes de WCF, l'intégration WCF-WF
  • etc...

Pour une liste complète des nouveautés il est intéressant de jeter un oeil à cette page MSDN.

Un exemple qui illustre les avancées plus ou moins méconnues de .NET 3.5, l'espace de noms System.ServiceModel.Syndication dans la dll System.ServiceModel.Web apporte de nouvelles facilités pour gérer des flux RSS. Et pour s'en convaincre quelques lignes de codes :

SyndicationFeed feed;
using (var r = XmlReader.Create(https://www.e-naxos.com/Blog/syndication.axd))
{ feed = SyndicationFeed.Load(r); }

C'est tout ! Dans la variable "feed" on a tout ce qu'il faut pour travailler sur le flux RSS.

Vous pensez que je triche et que "travailler sur le flux RSS" c'est certainement pas si simple que ça ?. Bon, allez, c'est parce que c'est vous : compliquons même la chose et, à partir de ce flux, affichons le titre de tous les billets qui parlent de LINQ dans leur corps. Voici le code complet près à compiler :

using System;
using System.Linq;
using System.ServiceModel.Syndication;
using System.Xml;

namespace NET35RSS
{ class Program
  {
     static void Main()
     {
        SyndicationFeed feed;
        using (var r = XmlReader.Create(
https://www.e-naxos.com/Blog/syndication.axd))
        { feed = SyndicationFeed.Load(r); }
           if (feed==null) { Console.WriteLine("Flux vide."); return; }
           Console.WriteLine(feed.Title.Text);
           Console.WriteLine(feed.Description.Text+"\n");
           var q = from item in feed.Items
           where item.Summary.Text.ToUpper().Contains("LINQ") select item;
           foreach (var item in q) Console.WriteLine(item.Title.Text);
       }
    }
}

Ajoutez une petite saisie pour le mot à chercher au lieu d'un codage en dur ("LINQ" dans cet exemple) et un petit fichier paramètre pour y stocker la liste des blogs que vous connaissez, et en deux minutes vous aurez un puissant outil de recherche capable de vous lister toutes les billets de vos blogs préférés qui parlent de tel ou tel sujet...

C'est pas génial ça ?

Si, ça l'est, bien sûr ! Alors Stay Tuned !

pour les paresseux du clavier, le projet VS 2008 : NET35RSS.zip (5,36 kb)