Perché un Regexp memorizzato nella cache supera quello compilato?
-
06-07-2019 - |
Domanda
Questa è solo una domanda per soddisfare la mia curiosità. Ma per me è interessante.
Ho scritto questo piccolo benchmark semplice. Chiama 3 varianti dell'esecuzione di Regexp in un ordine casuale alcune migliaia di volte:
Fondamentalmente, uso lo stesso modello ma in modi diversi.
-
Il tuo modo ordinario senza
RegexOptions
. A partire da .NET 2.0 questi non vengono memorizzati nella cache. Ma dovrebbe essere " memorizzato nella cache " perché è contenuto in un ambito piuttosto globale e non ripristinato. -
Con
RegexOptions.Compiled
-
Con una chiamata al
Regex.Match statico (modello, input)
che viene memorizzato nella cache in .NET 2.0
Ecco il codice:
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());
}
Ogni volta che lo chiamo il risultato è sulla falsariga di:
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
Quindi il gioco è fatto. Non molto, ma circa il 7-8% di differenza.
Non è l'unico mistero. Non posso spiegare perché il primo modo sarebbe molto più lento perché non viene mai rivalutato ma tenuto in una variabile statica globale.
A proposito, questo è su .Net 3.5 e Mono 2.2 che si comportano esattamente allo stesso modo. Su Windows.
Quindi, qualche idea, perché la variante compilata potrebbe persino restare indietro?
Edit1:
Dopo aver corretto il codice, i risultati ora appaiono così:
<*>Il che praticamente oscura anche tutte le altre domande.
Grazie per le risposte.
Soluzione
Nella versione Regex.Match stai cercando l'input nel pattern. Prova a scambiare i parametri in giro.
var m3 = Regex.Match(pattern, item); // Wrong
var m3 = Regex.Match(item, pattern); // Correct
Altri suggerimenti
L'ho notato simile comportamento. Mi chiedevo anche perché la versione compilata sarebbe più lenta, ma ho notato che sopra un certo numero di chiamate, la versione compilata è più veloce. Così ho scavato in Reflector e ho notato che per un Regex compilato, c'è ancora una piccola configurazione che viene eseguita alla prima chiamata (in particolare, creando un'istanza dell'appropriato RegexRunner
oggetto).
Nel mio test, ho scoperto che se ho spostato sia la funzione di costruzione che una prima chiamata a eliminazione diretta sulla regex al di fuori del timer, la regex compilata ha vinto indipendentemente da quante iterazioni ho eseguito.
Per inciso, la memorizzazione nella cache che il framework sta eseguendo quando si utilizzano metodi Regex
statici è un'ottimizzazione che è necessaria solo quando si utilizzano metodi Regex
statici. Questo perché ogni chiamata a un metodo Regex
statico crea un nuovo oggetto Regex
. Nel costruttore della classe Regex
deve analizzare il modello. La memorizzazione nella cache consente alle chiamate successive di metodi Regex
statici di riutilizzare il RegexTree
analizzato dalla prima chiamata, evitando così la fase di analisi.
Quando usi i metodi di istanza su un singolo oggetto Regex
, questo non è un problema. L'analisi viene comunque eseguita solo una volta (quando si crea l'oggetto). Inoltre, puoi evitare di eseguire tutto l'altro codice nel costruttore, nonché l'allocazione dell'heap (e la successiva garbage collection).
Martin Brown notato che hai notato ha invertito gli argomenti con la tua chiamata statica Regex
(buona notizia, Martin). Penso che scoprirai che se lo risolvi, il regex di istanza (non compilato) batterà ogni volta le chiamate statiche. Dovresti anche scoprire che, date le mie conclusioni sopra, anche l'istanza compilata batterà quella non compilata.
MA : dovresti davvero leggere Il post di Jeff Atwood sulle regex compilate prima di applicare ciecamente quell'opzione a ogni regex che crei.
Se abbini costantemente la stessa stringa utilizzando lo stesso modello, ciò potrebbe spiegare perché una versione memorizzata nella cache è leggermente più veloce di una versione compilata.
Questo è dalla documentazione;
https://msdn.microsoft.com /en-us/library/gg578045(v=vs.110).aspx
quando viene chiamato un metodo espressione regolare statica e il normale espressione non può essere trovata nella cache, il motore delle espressioni regolari converte l'espressione regolare in una serie di codici operativi e negozi nella cache . Quindi converte questi codici operativi in ??MSIL che il compilatore JIT può eseguirli. Interpretato regolarmente le espressioni riducono i tempi di avvio al costo di tempi di esecuzione più lenti . Per questo motivo, sono utilizzati al meglio quando l'espressione regolare è utilizzato in un numero limitato di chiamate di metodo o se il numero esatto di le chiamate ai metodi di espressione regolare sono sconosciute ma dovrebbero esserlo piccolo. All'aumentare del numero di chiamate al metodo, aumenta il rendimento dal tempo di avvio ridotto viene superato dall'esecuzione più lenta velocità.
Contrariamente alle espressioni regolari interpretate, compilato regolare le espressioni aumentano il tempo di avvio ma eseguono singoli metodi di corrispondenza dei modelli più veloci . Di conseguenza, il vantaggio in termini di prestazioni che risulta dalla compilazione dell'espressione regolare aumenta in proporzione al numero di metodi di espressione regolare chiamati.
Per riassumere, ti consigliamo di utilizzare espressioni regolari interpretate quando chiami metodi di espressione regolari con una specifica espressione regolare relativamente poco frequente.
Dovresti usare espressioni regolari compilate quando chiami regolarmente metodi di espressione con un'espressione regolare specifica relativamente frequentemente.
Come rilevare?
La soglia esatta alla quale rallenta l'esecuzione più lenta le espressioni regolari interpretate superano i guadagni dal loro ridotto tempo di avvio o la soglia alla quale i tempi di avvio più lenti di le espressioni regolari compilate superano i guadagni dal loro più veloce velocità di esecuzione, è difficile da determinare. Dipende da una varietà di fattori, tra cui la complessità dell'espressione regolare e il dati specifici che elabora. Per determinare se interpretato o le espressioni regolari compilate offrono le migliori prestazioni per il tuo particolare scenario applicativo, è possibile utilizzare la classe Cronometro per confrontare i loro tempi di esecuzione .
Espressioni regolari compilate:
Si consiglia di compilare espressioni regolari in un assembly in le seguenti situazioni:
- Se sei uno sviluppatore di componenti che lo desidera per creare una libreria di espressioni regolari riutilizzabili.
- Se ti aspetti i metodi di corrispondenza dei pattern della tua espressione regolare da chiamare un numero indeterminato di volte - ovunque da una o due volte a migliaia o decine di migliaia di volte. A differenza di compilato o espressioni regolari interpretate, espressioni regolari che vengono compilate separare gli assiemi offre prestazioni coerenti a prescindere del numero di chiamate al metodo.