Pourquoi une Regexp en cache surpasse-t-elle une compilation?
-
06-07-2019 - |
Question
Ceci est juste une question pour satisfaire ma curiosité. Mais pour moi, c’est intéressant.
J'ai écrit ce petit repère simple. Il appelle plusieurs milliers de fois l’exécution de Regexp dans un ordre aléatoire:
En principe, j'utilise le même modèle mais de différentes manières.
-
Votre façon habituelle sans
RegexOptions
. À partir de .NET 2.0, ceux-ci ne sont pas mis en cache. Mais devrait être "mis en cache" car il est maintenu dans une étendue plutôt globale et non réinitialisé. -
Avec
RegexOptions.Compiled
-
Avec un appel au statique
Regex.Match (motif, entrée)
qui est mis en cache dans .NET 2.0
Voici le code:
static List<string> Strings = new List<string>();
static string pattern = ".*_([0-9]+)\\.([^\\.]) Not compiled and not automatically cached:
Total milliseconds: 6185,2704
Adjusted milliseconds: 6185,2704
Compiled and not automatically cached:
Total milliseconds: 2562,2519
Adjusted milliseconds: 2551,56949184038
Not compiled and automatically cached:
Total milliseconds: 2378,823
Adjusted milliseconds: 2336,3187176891
quot;;
static Regex Rex = new Regex(pattern);
static Regex RexCompiled = new Regex(pattern, RegexOptions.Compiled);
static Random Rand = new Random(123);
static Stopwatch S1 = new Stopwatch();
static Stopwatch S2 = new Stopwatch();
static Stopwatch S3 = new Stopwatch();
static void Main()
{
int k = 0;
int c = 0;
int c1 = 0;
int c2 = 0;
int c3 = 0;
for (int i = 0; i < 50; i++)
{
Strings.Add("file_" + Rand.Next().ToString() + ".ext");
}
int m = 10000;
for (int j = 0; j < m; j++)
{
c = Rand.Next(1, 4);
if (c == 1)
{
c1++;
k = 0;
S1.Start();
foreach (var item in Strings)
{
var m1 = Rex.Match(item);
if (m1.Success) { k++; };
}
S1.Stop();
}
else if (c == 2)
{
c2++;
k = 0;
S2.Start();
foreach (var item in Strings)
{
var m2 = RexCompiled.Match(item);
if (m2.Success) { k++; };
}
S2.Stop();
}
else if (c == 3)
{
c3++;
k = 0;
S3.Start();
foreach (var item in Strings)
{
var m3 = Regex.Match(item, pattern);
if (m3.Success) { k++; };
}
S3.Stop();
}
}
Console.WriteLine("c: {0}", c1);
Console.WriteLine("Total milliseconds: " + (S1.Elapsed.TotalMilliseconds).ToString());
Console.WriteLine("Adjusted milliseconds: " + (S1.Elapsed.TotalMilliseconds).ToString());
Console.WriteLine("c: {0}", c2);
Console.WriteLine("Total milliseconds: " + (S2.Elapsed.TotalMilliseconds).ToString());
Console.WriteLine("Adjusted milliseconds: " + (S2.Elapsed.TotalMilliseconds*((float)c2/(float)c1)).ToString());
Console.WriteLine("c: {0}", c3);
Console.WriteLine("Total milliseconds: " + (S3.Elapsed.TotalMilliseconds).ToString());
Console.WriteLine("Adjusted milliseconds: " + (S3.Elapsed.TotalMilliseconds*((float)c3/(float)c1)).ToString());
}
Chaque fois que je l'appelle, le résultat est similaire à:
Not compiled and not automatically cached: Total milliseconds: 6456,5711 Adjusted milliseconds: 6456,5711 Compiled and not automatically cached: Total milliseconds: 2668,9028 Adjusted milliseconds: 2657,77574842168 Not compiled and automatically cached: Total milliseconds: 6637,5472 Adjusted milliseconds: 6518,94897724836
Donc là vous l'avez. Pas beaucoup, mais environ 7-8% de différence.
Ce n’est pas le seul mystère. Je ne peux pas expliquer pourquoi la première méthode serait beaucoup plus lente, car elle n'est jamais réévaluée mais conservée dans une variable statique globale.
À propos, il s’agit de .Net 3.5 et Mono 2.2 qui se comportent exactement de la même manière. Sous Windows.
Alors, des idées, pourquoi la variante compilée serait même en retard?
EDIT1:
Après avoir corrigé le code, les résultats se présentent comme suit:
<*>Ce qui obsolète à peu près toutes les autres questions.
Merci pour les réponses.
La solution
Dans la version Regex.Match, vous recherchez l’entrée dans le motif. Essayez d’échanger les paramètres.
var m3 = Regex.Match(pattern, item); // Wrong
var m3 = Regex.Match(item, pattern); // Correct
Autres conseils
j'ai remarqué similaire comportement. Je me suis aussi demandé pourquoi la version compilée serait plus lente, mais j'ai remarqué qu'au-delà d'un certain nombre d'appels, la version compilée est plus rapide. Alors j’ai creusé dans Reflector un peu, et je l’ai remarqué pour un Regex compilé, il y a encore une petite configuration effectuée au premier appel (en particulier, création d'une instance du objet RegexRunner
).
Lors de mon test, j'ai constaté que si je déplaçais à la fois le constructeur et un appel initial à la regex en dehors du début du minuteur, la regex compilée gagnait quel que soit le nombre d'itérations que j'avais exécutées.
Incidemment, la mise en cache effectuée par la structure lors de l’utilisation de méthodes statiques Regex
est une optimisation nécessaire uniquement lors de l’utilisation de méthodes statiques Regex
. En effet, chaque appel à une méthode statique Regex
crée un nouvel objet Regex
. Dans le constructeur de la classe Regex
, il doit analyser le motif. La mise en cache permet aux appels ultérieurs de méthodes Regex
statiques de réutiliser le RegexTree
analysé depuis le premier appel, évitant ainsi l’analyse.
Lorsque vous utilisez des méthodes d'instance sur un seul objet Regex
, le problème ne se pose pas. L'analyse n'est encore effectuée qu'une fois (lorsque vous créez l'objet). De plus, vous évitez d’exécuter tout le code du constructeur, ainsi que l’allocation de tas (et le ramassage ultérieur).
Martin Brown remarqué que vous a inversé les arguments de votre appel statique Regex
(bonne réponse, Martin). Je pense que vous constaterez que si vous corrigez cela, l'instance regex (non compilée) battra les appels statiques à chaque fois. Vous devriez également constater que, compte tenu de mes conclusions ci-dessus, l'instance compilée vaincra également l'instance non compilée.
MAIS : vous devriez vraiment lire Le message de Jeff Atwood sur les expressions rationnelles compilées avant d'appliquer aveuglément cette option à toutes les expressions rationnelles que vous créez.
Si vous faites constamment correspondre la même chaîne en utilisant le même modèle, cela peut expliquer pourquoi une version en cache est légèrement plus rapide qu'une version compilée.
Ceci provient de la documentation;
https://msdn.microsoft.com /en-us/library/gg578045(v=vs.110).aspx
lorsqu'une méthode expression régulière statique est appelée et que la méthode régulière expression introuvable dans le cache, le moteur des expressions régulières convertit l'expression régulière en un ensemble de codes d'opération et de magasins dans la mémoire cache . Il convertit ensuite ces codes d'opération en MSIL afin que le compilateur JIT peut les exécuter. régulier interprété les expressions réduisent le temps de démarrage au détriment du temps d'exécution . De ce fait, ils sont mieux utilisés lorsque l'expression régulière est utilisé dans un petit nombre d'appels de méthode , ou si le nombre exact de les appels aux méthodes d’expression régulière sont inconnus mais devraient être petit. Au fur et à mesure que le nombre d'appels de méthode augmente, le gain de performances du temps de démarrage réduit est dépassé par l'exécution plus lente vitesse.
Contrairement aux expressions rationnelles interprétées, compilées régulières les expressions augmentent le temps de démarrage mais exécutent individuellement méthodes de correspondance de modèle plus rapidement . En conséquence, le bénéfice de performance qui résulte de la compilation de l'expression régulière augmente en proportion du nombre de méthodes d'expression régulières appelées.
Pour résumer, nous vous recommandons d’utiliser des expressions rationnelles interprétées lorsque vous appelez des méthodes d’expression régulière avec un élément spécifique. expression régulière relativement peu fréquente.
Vous devez utiliser les expressions régulières compilées lorsque vous appelez des informations régulières. méthodes d'expression avec une expression régulière spécifique relativement fréquemment.
Comment détecter?
Le seuil exact auquel les vitesses d'exécution plus lentes de interprétées expressions régulières l'emportent sur les gains de leur réduction temps de démarrage, ou le seuil auquel les temps de démarrage plus lents de les expressions régulières compilées l'emportent sur les gains de leur rapidité les vitesses d'exécution, est difficile à déterminer. Cela dépend d'une variété de facteurs, y compris la complexité de l'expression régulière et la données spécifiques qu'il traite. Pour déterminer si interprété ou Les expressions régulières compilées offrent les meilleures performances pour votre scénario d’application particulier, vous pouvez utiliser la classe Stopwatch pour comparer leurs temps d'exécution .
Expressions régulières compilées:
Nous vous recommandons de compiler des expressions régulières dans un assemblage dans les situations suivantes:
- Si vous êtes un développeur de composants qui souhaite créer une bibliothèque d'expressions régulières réutilisables.
- Si vous vous attendez à Les méthodes de correspondance des modèles de votre expression régulière à appeler un nombre indéterminé de fois - de une ou deux fois à des milliers ou des dizaines de milliers de fois. Contrairement à compilé ou expressions régulières interprétées, expressions régulières compilées séparer les assemblages offrent des performances cohérentes indépendamment de du nombre d'appels de méthode.