2009-01-09 12 views
8

Esta es solo una pregunta para satisfacer mi curiosidad. Pero para mí es interesante.¿Por qué una Regexp en caché supera a una compilada?

Escribí este pequeño punto de referencia simple. Llama a 3 variantes de la ejecución Regexp en orden aleatorio miles de veces:

Básicamente, utilizo el mismo patrón pero de diferentes maneras.

  1. Su forma ordinaria sin ningún RegexOptions. Comenzando con .NET 2.0 estos no se almacenan en caché. Pero se debe "almacenar en caché" porque se mantiene en un ámbito bastante global y no se restablece.

  2. Con RegexOptions.Compiled

  3. Con una llamada a la estática Regex.Match(pattern, input) el que se pone en caché en el .NET 2,0

Aquí está el código:

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

Cada vez que llamo el resultado está en la línea de:

 
    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 

Así que ahí lo tienes. No mucho, pero alrededor de 7-8% de diferencia.

No es el único misterio. No puedo explicar por qué la primera forma sería mucho más lenta porque nunca se vuelve a evaluar sino que se mantiene en una variable estática global.

Por cierto, esto está en .Net 3.5 y Mono 2.2 que se comportan exactamente igual. En Windows.

Entonces, ¿alguna idea, por qué la variante compilada incluso se quedaría atrás?

EDIT1:

Después de fijar el código de los resultados ahora se ven así:

 
    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 

que prácticamente obsoleto todas las otras preguntas también.

Gracias por las respuestas.

Respuesta

4

En la versión Regex.Match está buscando la entrada en el patrón. Intenta intercambiar los parámetros.

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

Ah. :-) Gracias.Actualizaré mi publicación con los nuevos resultados en un minuto. – user51710

3

I noticed comportamiento similar. También me pregunté por qué la versión compilada sería más lenta, pero noté que por encima de una cierta cantidad de llamadas, la versión compilada es más rápida. Así que busqué un poco en Reflector, y noté que para un Regex compilado, todavía hay una pequeña configuración que se realiza en la primera llamada (específicamente, crear una instancia del objeto RegexRunner apropiado).

En mi prueba, encontré que si movía tanto el constructor como una llamada inicial descartable a la expresión regular fuera del inicio del temporizador, la expresión regular compilada no importaba cuántas iteraciones ejecutara.


Por cierto, el almacenamiento en caché que el marco está haciendo cuando se utiliza Regex métodos estáticos es una optimización que sólo se necesita cuando se utiliza Regex métodos estáticos.Esto se debe a que cada llamada a un método estático Regex crea un nuevo objeto Regex. En el constructor de la clase Regex, debe analizar el patrón. El almacenamiento en caché permite llamadas posteriores de métodos estáticos Regex para reutilizar el RegexTree analizado desde la primera llamada, evitando así el paso de análisis sintáctico.

Cuando utiliza métodos de instancia en un solo objeto Regex, entonces esto no es un problema. El análisis todavía se realiza una sola vez (cuando se crea el objeto). Además, evite ejecutar todos los demás códigos en el constructor, así como la asignación de montón (y la posterior recolección de elementos no utilizados).

Martin Brown noticed que invierte los argumentos a su llamada estática Regex (buena captura, Martin). Creo que encontrará que si corrige eso, la expresión regular instancia (no compilada) superará las llamadas estáticas cada vez. También debería encontrar que, dados mis hallazgos anteriores, la instancia compilada también superará a la no compilada.

PERO: Usted realmente debe leer Jeff Atwood's post en expresiones regulares compiladas antes de ir a ciegas aplicar esa opción para cada expresiones regulares se crea.

+0

Gracias por sus explicaciones. El paso inicial no parece incurrir en un alto costo en mi caso (ver los nuevos resultados). Leí la publicación de Jeff Atwood antes de publicar esto. Entonces soy consciente de los inconvenientes. En mi caso, la opción de compilación ayudaría aunque no tanto en el caso de uso estándar. – user51710

+0

** La publicación de Jeff Atwood ** se ha mudado: [Para compilar o no compilar * (03 de marzo de 2005) *] (http://blog.codinghorror.com/to-compile-or-not-to-compile/) – DavidRR

+0

Gracias, enlace actualizado. –

0

Si constantemente coincide con la misma cadena con el mismo patrón, eso puede explicar por qué una versión en caché es ligeramente más rápida que una compilada.

0

Esto es de la documentación;

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

cuando se llama a un método estática expresión regular y la expresión regular no puede ser encontrado en la memoria caché, el motor de expresiones regulares convierte la expresión regular para un conjunto de códigos de operación y almacena en el caché. Luego convierte estos códigos de operación a MSIL para que pueda ejecutarlos el compilador JIT. Las expresiones regulares interpretadas reducen el tiempo de inicio a costa de un tiempo de ejecución más lento. Debido a esto, ellos son utiliza mejor cuando la expresión regular es utilizado en un pequeño número de llamadas a métodos, o si el número exacto de llamadas a los métodos de expresión regular es desconocida, pero se espera que sea pequeña. A medida que aumenta el número de llamadas a métodos, la ganancia de rendimiento a partir del tiempo de inicio reducido queda superada por la velocidad de ejecución más lenta .

En contraste con las expresiones regulares interpretadas, compilados regulares expresiones aumentan el tiempo de puesta en marcha, pero se ejecutan métodos de comparación de patrones individuales más rápido. Como resultado, el beneficio de rendimiento que resulta de compilar la expresión regular aumenta en en proporción a la cantidad de métodos de expresión regulares llamados.


En resumen, recomendamos que utilice interpretarse expresiones regulares cuando se llama a métodos de expresión regular con una expresión regular específica relativamente con poca frecuencia.

que puedes usar expresiones regulares compiladas cuando llame regulares métodos de expresión con una expresión regular específica relativamente frecuencia.


Cómo detectar?

El umbral exacto en el que las velocidades de ejecución más lentas de expresiones regulares interpretadas son mayores que las ganancias derivadas de su reducida tiempo de inicio, o el umbral en el que los tiempos de inicio más lento de expresiones regulares compiladas son mayores que las ganancias de su rápido ejecución velocidades, es difícil de determinar. Depende de una variedad de factores, como la complejidad de la expresión regular y los datos específicos que procesa. Para determinar si las expresiones regulares compiladas interpretadas o ofrecen el mejor rendimiento para su escenario de aplicación particular , puede usar la clase Cronómetro de para comparar sus tiempos de ejecución.


Compilado expresiones regulares:

Recomendamos que se compila expresiones regulares para una asamblea en las siguientes situaciones:

  1. Si usted es un desarrollador de componentes que quiere para crear una biblioteca de expresiones regulares reutilizables.
  2. Si espera los métodos de coincidencia de patrón de su expresión regular se llamarán un número indeterminado de veces - desde una o dos veces hasta miles o decenas de miles de veces. A diferencia de las expresiones regulares interpretadas o interpretadas, las expresiones regulares que se compilan para ensamblajes separados ofrecen un rendimiento consistente independientemente del del número de llamadas a métodos.
Cuestiones relacionadas