Como o RegexOptions.Compiled funciona?

169

O que está acontecendo nos bastidores quando você marca uma expressão regular como uma a ser compilada? Como isso se compara / é diferente de uma expressão regular em cache?

Usando essas informações, como você determina quando o custo da computação é insignificante em comparação com o aumento de desempenho?

Prumo
fonte
bom recurso sobre as práticas recomendadas do Regex: docs.microsoft.com/en-us/dotnet/standard/base-types/…
CAD bloke

Respostas:

302

RegexOptions.Compiledinstrui o mecanismo de expressão regular a compilar a expressão regular em IL usando a geração de código leve ( LCG ). Essa compilação acontece durante a construção do objeto e o torna muito lento. Por sua vez, as correspondências usando a expressão regular são mais rápidas.

Se você não especificar esse sinalizador, sua expressão regular será considerada "interpretada".

Veja este exemplo:

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

    }
}

Ele realiza 4 testes em 3 expressões regulares diferentes. Primeiro, ele testa uma partida única (compilada vs não compilada). Segundo, ele testa correspondências repetidas que reutilizam a mesma expressão regular.

Os resultados na minha máquina (compilado na versão, sem depurador conectado)

1000 correspondências simples (construa Regex, corresponda e descarte)

Tipo | Plataforma | Número trivial | Simples verificação de e-mail | Verificação de email ext
-------------------------------------------------- ----------------------------
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 correspondências - reutilizando o objeto Regex

Tipo | Plataforma | Número trivial | Simples verificação de e-mail | Verificação de email ext
-------------------------------------------------- ----------------------------
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

Esses resultados mostram que expressões regulares compiladas podem ser até 60% mais rápidas nos casos em que você reutiliza o Regexobjeto. No entanto, em alguns casos, pode ser mais de três ordens de magnitude mais lenta a ser construída.

Também mostra que a versão x64 do .NET pode ser 5 a 6 vezes mais lenta quando se trata de compilação de expressões regulares.


A recomendação seria usar a versão compilada nos casos em que

  1. Você não se importa com o custo de inicialização do objeto e precisa do aumento extra de desempenho. (observe que estamos falando de frações de milissegundos aqui)
  2. Você se preocupa um pouco com o custo de inicialização, mas está reutilizando o objeto Regex tantas vezes que ele será compensado durante o ciclo de vida do aplicativo.

Chave inglesa em obras, o cache Regex

O mecanismo de expressão regular contém um cache LRU que contém as últimas 15 expressões regulares testadas usando os métodos estáticos da Regexclasse.

Por exemplo: Regex.Replace, Regex.Matchetc .. todos usam o cache de Regex.

O tamanho do cache pode ser aumentado pela configuração Regex.CacheSize. Ele aceita alterações de tamanho a qualquer momento durante o ciclo de vida do seu aplicativo.

Novas expressões regulares são armazenadas em cache apenas pelos auxiliares estáticos na classe Regex. Se você construir seus objetos, o cache será verificado (para reutilização e colidido), no entanto, a expressão regular que você construir não será anexada ao cache .

Esse cache é um cache LRU trivial , é implementado usando uma lista simples de dupla ligação. Se você aumentar para 5000 e usar 5000 chamadas diferentes nos auxiliares estáticos, toda construção de expressão regular rastreará as 5000 entradas para ver se ela foi armazenada em cache anteriormente. Há uma trava ao redor da verificação, portanto, a verificação pode diminuir o paralelismo e introduzir o bloqueio de threads.

O número é muito baixo para se proteger de casos como esse, embora em alguns casos você possa não ter escolha a não ser aumentá-lo.

Minha forte recomendação seria nunca passar a RegexOptions.Compiledopção a um ajudante estático.

Por exemplo:

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

O motivo é que você está arriscando muito a perda do cache LRU, o que desencadeará uma compilação muito cara . Além disso, você não tem idéia do que as bibliotecas das quais você depende estão fazendo, portanto, tem pouca capacidade de controlar ou prever o melhor tamanho possível do cache.

Veja também: blog da equipe BCL


Nota : isso é relevante para o .NET 2.0 e o .NET 4.0. Há algumas mudanças esperadas no 4.5 que podem fazer com que isso seja revisado.

Sam Saffron
fonte
11
Ótima resposta. Para meus próprios fins, costumo usar Compiledno código do site onde estou armazenando um Regexobjeto estático (em todo o aplicativo) . Portanto, o Regexúnico precisa ser construído uma vez quando o IIS inicia o aplicativo e, em seguida, é reutilizado milhares de vezes. Isso funciona bem desde que o aplicativo não seja reiniciado com frequência.
21312 Steve Steve
W00! Essas informações me ajudaram a acelerar meu processo de 8 a 13 horas para ~ 30 minutos. Obrigado!
Robert Christ
3
Ótima resposta Sam, você pode atualizar o que mudou na versão> 4.5? (Eu sei que você mudou sua pilha há um tempo ...)
gdoron está apoiando Monica
@gdoronissupportingMonica Houve algumas melhorias no desempenho do Regex no NET 5.0. Eu vi um post no blog para isso. Você pode conferir aqui
kapozade
42

Esta entrada no Blog da equipe BCL fornece uma boa visão geral: " Desempenho da expressão regular ".

Em resumo, existem três tipos de regex (cada um executando mais rápido que o anterior):

  1. interpretado

    rápido para criar em tempo real, lento para executar

  2. compilado (o que você parece perguntar)

    mais lento para criar em tempo real, rápido para executar (bom para execução em loops)

  3. pré-compilado

    crie no momento da compilação do seu aplicativo (sem penalidade na criação em tempo de execução), rápido para executar

Portanto, se você pretende executar a regex apenas uma vez, ou em uma seção não crítica ao desempenho do seu aplicativo (por exemplo, validação de entrada do usuário), está bem com a opção 1.

Se você deseja executar o regex em um loop (ou seja, análise de arquivo linha por linha), você deve seguir a opção 2.

Se você tiver muitas regexes que nunca serão alteradas para seu aplicativo e forem usadas intensamente, você poderá optar pela opção 3.

Tomalak
fonte
1
O número 3 poderia ser facilitado através de uma roslyn personalizadaCompileModule . Porra, eu preciso dar uma olhada mais profunda na nova plataforma.
Christian Gollhardt
9

Observe que o desempenho de expressões regulares desde o .NET 2.0 foi aprimorado com um cache MRU de expressões regulares não compiladas. O código da biblioteca Regex não reinterpreta sempre a mesma expressão regular não compilada.

Portanto, há potencialmente uma penalidade de desempenho maior com uma expressão regular compilada e dinâmica. Além de tempos de carregamento mais lentos, o sistema também usa mais memória para compilar a expressão regular para códigos de operação.

Essencialmente, o conselho atual é não compilar uma expressão regular ou compilá-las antecipadamente em um assembly separado.

Ref: BCL Team Blog Desempenho da expressão regular [David Gutierrez]

Robert Paulson
fonte
0

Espero que o código abaixo o ajude a entender o conceito de funções re.compile

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
fonte
Obrigado pela sua resposta, mas seu código está na linguagem Python . A pergunta era sobre a opção RegexOptions.Compiled do Microsoft .NET framework . Você pode ver a tag [ .net ] anexada abaixo da pergunta.
stomy
ah sim! Obrigado stomy
Daniel Muthupandi