2009-01-09 7 views
8

Dies ist nur eine Frage, um meine Neugier zu befriedigen. Aber für mich ist es interessant.Warum übertrifft ein zwischengespeicherter Regexp einen kompilierten?

Ich schrieb diesen kleinen einfachen Benchmark. Es ruft 3 Varianten der Regexp-Ausführung in zufälliger Reihenfolge ein paar tausend Mal auf:

Grundsätzlich verwende ich das gleiche Muster, aber auf verschiedene Arten.

  1. Ihre normale Art und Weise ohne RegexOptions. Ab .NET 2.0 werden diese nicht zwischengespeichert. Aber sollte "zwischengespeichert" werden, weil es in einem ziemlich globalen Bereich gehalten und nicht zurückgesetzt wird.

  2. Mit RegexOptions.Compiled

  3. Mit einem Aufruf der statischen Regex.Match(pattern, input) die 2,0

Hier in .NET zwischengespeichert Bekommt ist der Code:

static List<string> Strings = new List<string>();   
static string pattern = ".*_([0-9]+)\\.([^\\.])$"; 

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()); 
} 

Jedesmal, wenn ich es so nennen Das Ergebnis ist in etwa wie folgt:

 
    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 

Also da hast du es. Nicht viel, aber etwa 7-8% Unterschied.

Es ist nicht das einzige Geheimnis. Ich kann nicht erklären, warum der erste Weg so viel langsamer wäre, weil er nie neu bewertet wird, sondern in einer globalen statischen Variablen gehalten wird.

Übrigens ist dies auf .Net 3.5 und Mono 2.2, die sich genau gleich verhalten. Unter Windows.

Also, irgendwelche Ideen, warum die kompilierte Variante sogar zurückfallen würde?

EDIT1:

Nach der Fixierung des Codes die Ergebnisse nun wie folgt aussehen:

 
    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 

die so ziemlich alle anderen Fragen auch obsoletes.

Danke für die Antworten.

Antwort

4

In der Regex.Match-Version suchen Sie nach der Eingabe im Muster. Versuchen Sie, die Parameter zu umgehen.

var m3 = Regex.Match(pattern, item); // Wrong 
var m3 = Regex.Match(item, pattern); // Correct 
+0

Ah. :-) Danke.Ich werde meine Post mit den neuen Ergebnissen in einer Minute aktualisieren. – user51710

3

I noticed ähnliches Verhalten. Ich fragte mich auch, warum die kompilierte Version langsamer sein würde, aber bemerkte, dass die kompilierte Version ab einer bestimmten Anzahl von Aufrufen schneller ist. Also habe ich ein wenig in Reflector gegraben, und mir ist aufgefallen, dass es für einen kompilierten Regex immer noch ein kleines Setup gibt, das beim ersten Aufruf ausgeführt wird (genauer gesagt, eine Instanz des entsprechenden RegexRunner Objekts erstellen).

In meinem Test fand ich, dass, wenn ich sowohl den Konstruktor als auch einen anfänglichen Wegwerf-Aufruf an die Regex außerhalb des Timer-Starts bewegte, der kompilierte Regex gewann, egal wie viele Iterationen ich ausführte.


übrigens das Caching, dass der Rahmen tut, wenn statische Regex Methoden ist eine Optimierung, die nur benötigt, wenn statische Regex Methoden.Dies liegt daran, dass bei jedem Aufruf einer statischen Methode Regex ein neues Objekt Regex erstellt wird. Im Konstruktor der Klasse Regex muss das Muster analysiert werden. Die Zwischenspeicherung ermöglicht nachfolgende Aufrufe statischer Regex-Methoden, um die vom ersten Aufruf analysierte RegexTree wiederzuverwenden, wodurch der Parsing-Schritt vermieden wird.

Wenn Sie Instanzmethoden für ein einzelnes Objekt Regex verwenden, ist dies kein Problem. Das Parsen wird nur einmal ausgeführt (wenn Sie das Objekt erstellen). Darüber hinaus müssen Sie vermeiden, den gesamten anderen Code im Konstruktor auszuführen, sowie die Heap-Zuweisung (und die nachfolgende Speicherbereinigung).

Martin Brown noticed, dass Sie die Argumente zu Ihrem statischen Regex Anruf umgekehrt (guter Fang, Martin). Ich denke, Sie werden feststellen, dass, wenn Sie das beheben, die Instanz (nicht kompiliert) Regex die statischen Aufrufe jedes Mal schlagen wird. Sie sollten auch feststellen, dass die kompilierte Instanz auch die nicht kompilierten Instanzen übertrifft.

ABER: Sie sollten wirklich Jeff Atwood's post auf kompilierten Regexen lesen, bevor Sie diese Option blind auf jede von Ihnen erstellte Regex anwenden.

+0

Vielen Dank für Ihre Erläuterungen. Der erste Schritt scheint in meinem Fall nicht zu viel kosten (siehe die neuen Ergebnisse). Ich habe Jeff Atwoods Beitrag gelesen, bevor ich diesen Artikel gepostet habe. Ich bin mir also der Nachteile bewusst. In meinem Fall würde die Option Kompilieren helfen, wenn auch nicht im Standard-Anwendungsfall. – user51710

+0

** Jeff Atwoods Post ** ist umgezogen: [Um zu kompilieren oder nicht zu kompilieren * (03 Mar 2005) *] (http://blog.codinghorror.com/to-compile-or-not-to-compile/) – DavidRR

+0

Danke, aktualisierter Link. –

0

Wenn Sie ständig die gleiche Zeichenfolge mit dem gleichen Muster übereinstimmen, kann dies erklären, warum eine zwischengespeicherte Version etwas schneller als eine kompilierte Version ist.

0

Dies ist aus der Dokumentation;

https://msdn.microsoft.com/en-us/library/gg578045(v=vs.110).aspx

wenn eine statische reguläre Ausdruck Methode aufgerufen wird und der reguläre Ausdruck kann nicht im Cache, der reguläre Ausdruck Motor gefunden wird wandelt den regulären Ausdruck in einen Satz von Operationscodes und speichert sie im Cache. Es wandelt dann diese Operationscodes in MSIL so um, dass der JIT-Compiler sie ausführen kann. Interpretierte reguläre Ausdrücke reduzieren die Startzeit auf Kosten einer langsameren Ausführungszeit. Aus diesem Grunde sind sie am besten, wenn der reguläre Ausdruck in einer kleinen Anzahl von Verfahren verwendet wird, ist nennt, oder wenn die genaue Anzahl der Anrufe zu regulärer Ausdruck Methoden ist nicht bekannt, aber seine klein erwartet. Wenn die Anzahl der Methodenaufrufe ansteigt, wird der Leistungsgewinn von der reduzierten Startzeit durch die langsamere Ausführung Geschwindigkeit übertroffen.

Im Gegensatz zu regulären Ausdrücken interpretiert, kompilierte reguläre Ausdrücke Startzeit erhöhen, sondern einzelnes Pattern-Matching-Verfahren schneller auszuführen. Als Ergebnis erhöht sich der Leistungsvorteil , der sich aus dem Kompilieren des regulären Ausdrucks ergibt, in proportional zur Anzahl der aufgerufenen regulären Ausdrucksmethoden.


Zusammenfassend empfehlen wir die Verwendung von regulären Ausdrücke interpretiert, wenn man relativ selten reguläre Ausdruck Methoden mit einem bestimmten regulären Ausdruck nennen.

Sie sollten kompilierte reguläre Ausdrücke verwenden, wenn Sie regelmäßig Expressionsverfahren mit einem bestimmten regulären Ausdruck relativ häufig anrufen.


Wie zu erkennen?

Die genaue Schwelle, bei der die langsameren Ausführungsgeschwindigkeiten von interpretierten reguläre Ausdrücke Gewinne aus ihrer reduzierten Startzeit überwiegen, oder die Schwelle, bei der die langsameren Anlaufzeiten von kompilierten reguläre Ausdrücke Gewinne aus ihrer schnelleren Ausführung überwiegen Geschwindigkeiten, ist schwer zu bestimmen. Es hängt von einer Vielfalt von Faktoren ab, einschließlich der Komplexität des regulären Ausdrucks und der spezifischen Daten, die es verarbeitet. Um zu ermitteln, ob interpretierte oder kompilierte reguläre Ausdrücke die beste Leistung für Ihr spezielles Anwendungsszenario bieten, können Sie die Stoppuhrklasse verwenden, um ihre Ausführungszeiten zu vergleichen.


Zusammengestellt Reguläre Ausdrücke:

Wir empfehlen, dass Sie reguläre Ausdrücke zu einer Versammlung in die folgenden Situationen kompilieren:

  1. Wenn Sie ein Komponenten-Entwickler sind die will Erstellen einer Bibliothek wiederverwendbarer regulärer Ausdrücke.
  2. Wenn Sie erwarten, Ihre regulären Ausdruck der Muster-Matching-Methoden eine unbestimmte Anzahl von Mal aufgerufen werden - überall von einmal oder zweimal bis Tausende oder Zehntausende Male. Im Gegensatz zu kompilierten oder interpretierten regulären Ausdrücken bieten reguläre Ausdrücke, die in separaten Assemblys kompiliert werden, eine Leistung, die unabhängig von der Anzahl der Methodenaufrufe ist.
Verwandte Themen