¿Cómo funciona RegexOptions.Compiled?

169

¿Qué sucede detrás de escena cuando marca una expresión regular como una para compilar? ¿Cómo se compara / es diferente de una expresión regular en caché?

Con esta información, ¿cómo determina cuándo el costo de la computación es insignificante en comparación con el aumento del rendimiento?

Beto
fuente
buen recurso sobre las mejores prácticas de Regex
CAD

Respuestas:

302

RegexOptions.Compiledindica al motor de expresión regular que compile la expresión de expresión regular en IL utilizando la generación de código ligero ( LCG ). Esta compilación ocurre durante la construcción del objeto y lo ralentiza mucho . A su vez, las coincidencias que utilizan la expresión regular son más rápidas.

Si no especifica este indicador, su expresión regular se considera "interpretada".

Toma este ejemplo:

public static void TimeAction(string description, int times, Action func)
{
    // warmup
    func();

    var watch = new Stopwatch();
    watch.Start();
    for (int i = 0; i < times; i++)
    {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
}

static void Main(string[] args)
{
    var simple = "^\\d+$";
    var medium = @"^((to|from)\W)?(?<url>http://[\w\.:]+)/questions/(?<questionId>\d+)(/(\w|-)*)?(/(?<answerId>\d+))?";
    var complex = @"^(([^<>()[\]\\.,;:\s@""]+"
      + @"(\.[^<>()[\]\\.,;:\s@""]+)*)|("".+""))@"
      + @"((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}"
      + @"\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+"
      + @"[a-zA-Z]{2,}))$";


    string[] numbers = new string[] {"1","two", "8378373", "38737", "3873783z"};
    string[] emails = new string[] { "[email protected]", "sss@s", "[email protected]", "[email protected]" };

    foreach (var item in new[] {
        new {Pattern = simple, Matches = numbers, Name = "Simple number match"},
        new {Pattern = medium, Matches = emails, Name = "Simple email match"},
        new {Pattern = complex, Matches = emails, Name = "Complex email match"}
    })
    {
        int i = 0;
        Regex regex;

        TimeAction(item.Name + " interpreted uncached single match (x1000)", 1000, () =>
        {
            regex = new Regex(item.Pattern);
            regex.Match(item.Matches[i++ % item.Matches.Length]);
        });

        i = 0;
        TimeAction(item.Name + " compiled uncached single match (x1000)", 1000, () =>
        {
            regex = new Regex(item.Pattern, RegexOptions.Compiled);
            regex.Match(item.Matches[i++ % item.Matches.Length]);
        });

        regex = new Regex(item.Pattern);
        i = 0;
        TimeAction(item.Name + " prepared interpreted match (x1000000)", 1000000, () =>
        {
            regex.Match(item.Matches[i++ % item.Matches.Length]);
        });

        regex = new Regex(item.Pattern, RegexOptions.Compiled);
        i = 0;
        TimeAction(item.Name + " prepared compiled match (x1000000)", 1000000, () =>
        {
            regex.Match(item.Matches[i++ % item.Matches.Length]);
        });

    }
}

Realiza 4 pruebas en 3 expresiones regulares diferentes. Primero, prueba una sola vez una partida (compilada vs no compilada). En segundo lugar, prueba las coincidencias repetidas que reutilizan la misma expresión regular.

Los resultados en mi máquina (compilados en la versión, sin depurador adjunto)

1000 partidos individuales (construir Regex, Match y disponer)

Tipo | Plataforma | Número trivial | Verificación simple de correo electrónico | Ext Email Check
-------------------------------------------------- ----------------------------
Interpretado | x86 | 4 ms | 26 ms | 31 ms
Interpretado | x64 | 5 ms | 29 ms | 35 ms
Compilado | x86 | 913 ms | 3775 ms | 4487 ms
Compilado | x64 | 3300 ms | 21985 ms | 22793 ms

1,000,000 coincidencias - reutilizando el objeto Regex

Tipo | Plataforma | Número trivial | Verificación simple de correo electrónico | Ext Email Check
-------------------------------------------------- ----------------------------
Interpretado | x86 | 422 ms | 461 ms | 2122 ms
Interpretado | x64 | 436 ms | 463 ms | 2167 ms
Compilado | x86 | 279 ms | 166 ms | 1268 ms
Compilado | x64 | 281 ms | 176 ms | 1180 ms

Estos resultados muestran que las expresiones regulares compiladas pueden ser hasta un 60% más rápidas para los casos en los que reutiliza el Regexobjeto. Sin embargo, en algunos casos puede ser más de 3 órdenes de magnitud más lento de construir.

También muestra que la versión x64 de .NET puede ser de 5 a 6 veces más lenta cuando se trata de compilar expresiones regulares.


La recomendación sería utilizar la versión compilada en los casos en que

  1. No le importa el costo de inicialización de objetos y necesita un aumento de rendimiento adicional. (Tenga en cuenta que estamos hablando de fracciones de milisegundos aquí)
  2. Le importa un poco el costo de inicialización, pero está reutilizando el objeto Regex tantas veces que lo compensará durante el ciclo de vida de la aplicación.

Llave en proceso, el caché Regex

El motor de expresiones regulares contiene un caché LRU que contiene las últimas 15 expresiones regulares que se probaron utilizando los métodos estáticos en la Regexclase.

Por ejemplo: Regex.Replace, Regex.Matchetc .. todo el uso de la caché de expresiones regulares.

El tamaño de la memoria caché se puede aumentar mediante la configuración Regex.CacheSize. Acepta cambios de tamaño en cualquier momento durante el ciclo de vida de su aplicación.

Las nuevas expresiones regulares solo son almacenadas en caché por los ayudantes estáticos en la clase Regex. Sin embargo, si construye sus objetos, la memoria caché se verifica (para su reutilización y eliminación), la expresión regular que construye no se agrega a la memoria caché .

Este caché es un caché LRU trivial , se implementa utilizando una lista simple de doble enlace. Si lo aumenta a 5000 y usa 5000 llamadas diferentes en los ayudantes estáticos, cada construcción de expresión regular rastreará las 5000 entradas para ver si se ha almacenado previamente en caché. Hay un bloqueo alrededor del cheque, por lo que el cheque puede disminuir el paralelismo e introducir el bloqueo de hilos.

El número se establece bastante bajo para protegerse de casos como este, aunque en algunos casos puede que no tenga más remedio que aumentarlo.

Mi recomendación fuerte sería nunca pasarle la RegexOptions.Compiledopción a un ayudante estático.

Por ejemplo:

\\ WARNING: bad code
Regex.IsMatch("10000", @"\\d+", RegexOptions.Compiled)

La razón es que está arriesgando mucho una falla en la caché de LRU que desencadenará una compilación súper costosa . Además, no tiene idea de lo que están haciendo las bibliotecas de las que depende, por lo que tiene poca capacidad para controlar o predecir el mejor tamaño posible de la memoria caché.

Ver también: blog del equipo BCL


Nota : esto es relevante para .NET 2.0 y .NET 4.0. Hay algunos cambios esperados en 4.5 que pueden hacer que esto se revise.

Sam Azafrán
fuente
11
Gran respuesta. Para mis propios fines, a menudo lo uso Compileden el código del sitio web donde realmente estoy almacenando un objeto estático (para toda la aplicación) Regex. Por lo tanto, Regexsolo se debe construir una vez cuando IIS inicia la aplicación, y luego se reutiliza miles de veces. Esto funciona bien siempre que la aplicación no se reinicie con frecuencia.
Steve Wortham
W00! Esta información me ayudó a acelerar mi proceso de 8 a 13 horas, a ~ 30 minutos. ¡Gracias!
Robert Christ
3
Gran respuesta Sam, ¿puedes actualizar sobre lo que ha cambiado en la versión> 4.5? (Sé que cambiaste tu stack hace un tiempo ...)
gdoron está apoyando a Monica
@gdoronissupportingMonica Ha habido algunas mejoras en el rendimiento de Regex en NET 5.0. Vi una publicación de blog para esto. Puedes verlo aquí
kapozade
42

Esta entrada en el Blog del equipo de BCL ofrece una buena descripción general: " Rendimiento de expresión regular ".

En resumen, hay tres tipos de expresiones regulares (cada una ejecutándose más rápido que la anterior):

  1. interpretado

    rápido para crear sobre la marcha, lento para ejecutar

  2. compilado (el que parece preguntar)

    más lento para crear sobre la marcha, rápido de ejecutar (bueno para la ejecución en bucles)

  3. precompilado

    cree en tiempo de compilación de su aplicación (sin penalización por creación en tiempo de ejecución), rápido de ejecutar

Por lo tanto, si tiene la intención de ejecutar la expresión regular solo una vez, o en una sección no crítica del rendimiento de su aplicación (es decir, validación de entrada del usuario), está bien con la opción 1.

Si tiene la intención de ejecutar la expresión regular en un bucle (es decir, análisis de archivo línea por línea), debe ir con la opción 2.

Si tiene muchas expresiones regulares que nunca cambiarán para su aplicación y se usan intensamente, puede optar por la opción 3.

Tomalak
fuente
1
El número 3 podría facilitarse a través de una roslyn personalizadaCompileModule . Maldición, necesito echar un vistazo más profundo a la nueva plataforma.
Christian Gollhardt
9

Cabe señalar que el rendimiento de las expresiones regulares desde .NET 2.0 se ha mejorado con un caché MRU de expresiones regulares sin compilar. El código de la biblioteca Regex ya no reinterpreta la misma expresión regular sin compilar cada vez.

Por lo tanto, existe potencialmente una mayor penalización de rendimiento con una expresión regular compilada y sobre la marcha. Además de tiempos de carga más lentos, el sistema también usa más memoria para compilar la expresión regular en códigos de operación.

Esencialmente, el consejo actual es no compilar una expresión regular o compilarlas por adelantado en un ensamblaje separado.

Ref: BCL Team Blog Rendimiento de expresión regular [David Gutiérrez]

Robert Paulson
fuente
0

Espero que el siguiente código lo ayude a comprender el concepto de recompilar funciones

import re

x="""101 COM    Computers
205 MAT   Mathematics
189 ENG   English
222 SCI Science
333 TA  Tamil
5555 KA  Kannada
6666  TL  Telugu
777777 FR French
"""

#compile reg expression / successfully compiled regex can be used in any regex 
#functions    
find_subject_code=re.compile("\d+",re.M)
#using compiled regex in regex function way - 1
out=find_subject_code.findall(x)
print(out)
#using compiled regex in regex function way - 2
out=re.findall(find_numbers,x)
print(out)

#few more eg:
#find subject name
find_subjectnames=re.compile("(\w+$)",re.M) 
out=find_subjectnames.findall(x)
print(out)


#find subject SHORT name
find_subject_short_names=re.compile("[A-Z]{2,3}",re.M) 
out=find_subject_short_names.findall(x)
print(out)
Daniel Muthupandi
fuente
Gracias por su respuesta, pero su código está en el lenguaje Python . La pregunta era sobre la opción Microsoft .NET Framework RegexOptions.Compiled . Puede ver la etiqueta [ .net ] adjunta debajo de la pregunta.
Stomy
¡Ohh sí! Gracias stomy
Daniel Muthupandi