Introduction : Le mythe de la vitesse des expressions régulières
\n\nSi vous avez déjà écrit du code en C# et utilisé des expressions régulières, vous avez sûrement été confronté au dilemme : utiliser \\d ou [0-9] pour rechercher des chiffres ? À première vue, il n'y a pas de différence — les deux trouvent des chiffres. Mais les développeurs expérimentés, qui peaufinent les performances jusqu'à la perfection, savent que \\d peut être considérablement plus lent que [0-9].
Sur Stack Overflow, cette question a recueilli des milliers de vues et des dizaines de commentaires. Aujourd'hui, nous ne nous contenterons pas de répéter la réponse, mais nous allons regarder sous le capot du moteur .NET Regex et comprendre pourquoi cela se produit et comment cela affecte votre code. Nous allons écrire de véritables benchmarks en C#, examiner le code IL et découvrir quand il faut s'inquiéter et quand on peut se détendre.
\n\nPrêts ? C'est parti !
\n\nQue sont \\d et [0-9] en C# ?
\n\nPour commencer — les bases. Dans la plupart des implémentations d'expressions régulières (y compris C#), \\d est un raccourci pour « n'importe quel chiffre ». Et [0-9] est une classe de caractères qui énumère explicitement la plage de '0' à '9'.
Il semble que ce soit la même chose. Mais le diable, comme toujours, se cache dans les détails.
\n\nUnicode vs ASCII
\n\nDans .NET (à partir de .NET Framework 4.5 et versions ultérieures), \\d correspond par défaut à tout caractère de la catégorie Unicode Nd (Number, Decimal Digit). Il ne s'agit pas seulement des chiffres arabes 0-9, mais aussi :
- \n
- Des chiffres devanagari (०१२३...) \n
- Des chiffres tamouls \n
- Des chiffres arabo-indiens orientaux \n
- Et des dizaines d'autres systèmes de numération \n
Alors que [0-9] est une plage de codes ASCII strictement définie (48-57). Uniquement 0,1,2,3,4,5,6,7,8,9. Pas d'Unicode.
Pourquoi la vitesse est-elle différente ? Analyse sous le capot
\n\nVenons-en maintenant à l'essentiel : pourquoi \\d est-il plus lent ? Analysons cela au niveau du moteur.
1. Nombre de vérifications
\n\nLorsque le moteur rencontre [0-9], il fait une chose : il vérifie si le code du caractère se trouve dans la plage 48-57. C'est une comparaison (ou deux, si l'on tient compte des limites).
Lorsque le moteur rencontre \\d, il doit :
- \n
- Obtenir la catégorie Unicode du caractère (c'est déjà plus coûteux qu'un simple int). \n
- La comparer à la catégorie
Nd. \n
La catégorie Unicode n'est pas simplement un nombre, c'est tout un indicateur qui est calculé via des tables internes. Pour les chiffres latins, le résultat sera le même, mais le chemin pour y parvenir est plus long.
\n\n2. Mise en cache et JIT
\n\nDans .NET, Regex est compilé en code intermédiaire (IL) ou même en code natif (si RegexOptions.Compiled est utilisé). Pour [0-9], le JIT peut générer une séquence d'instructions très simple : quelques comparaisons et un saut conditionnel. Pour \\d, il s'agit d'un appel à la méthode CharUnicodeInfo.GetUnicodeCategory suivi d'une comparaison.
L'appel de méthode entraîne des frais généraux : push des arguments, call, vérification de la pile. Même si la méthode est petite, elle reste plus lente qu'une comparaison intégrée.
\n\n3. Exemple d'un benchmark réel
\n\nÉcrivons un test simple en C# en utilisant BenchmarkDotNet (assurez-vous de l'installer via NuGet si vous voulez reproduire) :
\n\nusing BenchmarkDotNet.Attributes;\nusing BenchmarkDotNet.Running;\nusing System.Text.RegularExpressions;\n\npublic class RegexBenchmark\n{\n private static readonly Regex _digitPattern = new Regex(@"\\d", RegexOptions.Compiled);\n private static readonly Regex _rangePattern = new Regex(@"[0-9]", RegexOptions.Compiled);\n \n private const string TestString = "Hello123World456!";\n\n [Benchmark]\n public bool DigitPattern() => _digitPattern.IsMatch(TestString);\n\n [Benchmark]\n public bool RangePattern() => _rangePattern.IsMatch(TestString);\n}\n\nclass Program\n{\n static void Main() => BenchmarkRunner.Run<RegexBenchmark>();\n}\n\nRésultats (sur .NET