Notícias dos desenvolvedores: Vercel se prepara para React 19; Novos lançamentos jQuery e pnpm
20 de abril de 2024IA ambiental? ‘Ai Pin’ do Humane embarca no longo caminho de um sonho
21 de abril de 2024Sinto-me um pouco culpado por apontar as vantagens do uso de expressões regulares em vários posts, sem nunca mencionar o quão lentas elas podem ser.
Em muitos casos de aplicação, a velocidade do regex não é um problema. Está apenas detectando alguns problemas com um formulário. Mas quando a velocidade é importante, você é repentinamente escalado para o papel de um detetive em busca de um assassino do tempo. Isso pode forçá-lo a descobrir quais bits de código são ineficientes, mas ter que acelerar as coisas sob pressão de produção é uma ação arriscada.
Usarei exemplos de C#, mas o resultado final é que geralmente você precisa cuidar de como usar uma regex em qualquer linguagem que usar, e opções como compilar a regex podem ajudar.
Como estou comparando velocidades de execução, terei que usar algum tipo de ferramenta de benchmark para fazer comparações válidas. Felizmente, o BenchmarkDotNet existe. Funciona para aplicativos de console, que é tudo de que precisamos.
Continuarei usando o Visual Studio Code porque é melhor para criar e mostrar projetos sem precisar de uma solução. Para acelerar as coisas, usarei um modelo.
Abrindo o Warp, primeiro executo estas etapas:
Isso apenas nos prepara para um projeto chamado ReferênciaRegex usando o disponível modelo de referência para configurar um esqueleto de projeto adequado. Podemos ver os arquivos gerados no diretório:
Podemos então iniciar o VS Code com > code .
e inicie o IDE diretamente na área de trabalho do projeto.
Mas primeiro, vamos considerar algumas das tarefas de regex que executei em posts anteriores. Usamos um pequeno padrão complicado usando alternância e olhar em volta para provar como “eu antes de e exceto depois de c” é regularmente quebrado em inglês:
O padrão acima encontra exemplos de quebra procurando por “cie” ou “ei” sem o “c”. Observe que lookaround é uma daquelas funções do Regex que pode se comportar de maneira diferente em diferentes implementações e deve ser usada com moderação. Neste caso, usamos um olhar negativo para trás (?<!c)
para confirmar que o “ei” não é precedido de “c”, mas sem consumir esse “c”. Leia a postagem para mais detalhes.
Podemos inserir este texto e padrão de exemplo diretamente em nosso novo arquivo de modelo, Referência.cs:
using System; using System.Text.RegularExpressions; using BenchmarkDotNet; using BenchmarkDotNet.Attributes; namespace BenchmarkRegex { public class Benchmarks { private const string Pattern = @"(cie|(?<!c)ei)"; private const string GoodText = "Good: ceiling, receipt, deceive, chief, field, believe."; private const string BadText = "Bad: species, science, sufficient, seize, vein, weird."; static bool printMeOnce = false; (Benchmark) public void Scenario1() { // Implement your benchmark here var f = Regex.IsMatch(GoodText + BadText, Pattern); if (!printMeOnce) foreach (Match match in Regex.Matches(GoodText+BadText, Pattern, RegexOptions.None)) Console.WriteLine("Found '{0}' at position {1}", match.Value, match.Index); printMeOnce = true; } } }
Primeiro, verificamos se a partida funciona e se captura os seis casos.
Só podemos fazer benchmarking em aplicativos de console no modo de lançamento, o que é bom, para que possamos executar > dotnet run -C Release
na linha de comando Warp. Logo no log, temos a confirmação de que os seis casos foram detectados:
No final, obtemos o benchmark:
Ok, isso é ótimo. Claro, agora precisamos voltar ao nosso tópico, que é acelerar o regex. Portanto, o primeiro e bastante óbvio método é apenas fazer o padrão estático. Agora que confirmamos que o padrão funciona, podemos dispensar a impressão, que afinal deixou o benchmark muito lento!
.. private const string Pattern = @"(cie|(?<!c)ei)"; private static readonly string StaticPattern = @"(cie|(?<!c)ei)"; .. (Benchmark) public void Scenario1() { // Implement your benchmark here Regex.Matches(GoodText+BadText, Pattern, RegexOptions.None); } (Benchmark) public void Scenario2() { // Implement your benchmark here Regex.Matches(GoodText+BadText, StaticPattern, RegexOptions.None); } ..
Portanto, esperaríamos aproximadamente que o segundo cenário fosse um pouco mais rápido. E isso é:
(Sim, sem a impressão estamos na faixa dos nanossegundos.)
Agora que testamos o benchmarking, podemos testar a opção compilada:
private const string Pattern = @"(cie|(?<!c)ei)"; private static readonly string StaticPattern = @"(cie|(?<!c)ei)"; private static readonly Regex CompiledRegex = new(Pattern, RegexOptions.Compiled); .. (Benchmark) public void Scenario3() { CompiledRegex.Matches(GoodText+BadText); } ..
Então, como funciona esse benchmark?
Bem, isso é cerca de metade. Mas esta não é uma conclusão aberta e fechada. Há uma série de coisas acontecendo dentro e em torno disso que você precisa estar ciente.
Quando você começou a usar C#, você provavelmente se lembra de ter aprendido que ele foi convertido em um linguagem intermediária (IL ou MSIL) e que posteriormente foi compilado no formato nativo do seu sistema operacional via na hora certa (JIT) compilação. (Na época em que o C# foi lançado em 2000, isso parecia um pouco irrelevante, já que a Microsoft estava fortemente ligada ao Windows.)
Regex, no entanto, produz seus próprios nós, analisar árvores e operações que são então transformadas em IL. Lembre-se de que regex é uma tecnologia muito mais antiga que .NET – cerca de meio século. Em parte, é por isso que existem regras especiais para lidar com isso.
Sem o sinalizador Compile, um objeto regex instanciado é interpretado para um conjunto de operações internas conforme descrito acima. Quando um método no objeto é chamado (como Corresponder), só então esses códigos de operação são transformados em IL para que o compilador JIT possa executá-los. Isso é bom se houver poucas chamadas de regex feitas. Se a definição de regex for estáticoentão os códigos de operação são obtidos armazenado em cache. Por padrão, os últimos 15 usados mais recentemente são armazenados em cache. Se você realmente estiver usando muitos padrões, poderá alterar isso com o Regex.CacheSizepropriedade.
Se o sinalizador Compile for usado, as expressões regulares pré-compiladas aumentam o tempo de inicialização, mas executam métodos individuais de correspondência de padrões mais rapidamente. Isso é bom se você usar determinados padrões repetidamente.
Você pode criar um objeto e padrão regex, compilá-lo e salvá-lo em um montagem autônoma. Você pode ligar para o Regex.CompileToAssembly método para compilar e salvá-lo. Mas faz sentido considerar isso em tempo de design, à medida que você divide seu aplicativo em assemblies separados.
Em resumo, a conclusão sensata a se chegar é que a regex não deve ser usada em áreas com tempo crítico e próximo a elas. Se você executar poucas expressões, é melhor executá-las da maneira usual interpretada. Se você executa muito os mesmos padrões, use o sinalizador Compile ou coloque-os em uma montagem separada. Em última análise, se você puder isolar os métodos regex e usar benchmarks para verificar comparações, poderá pegar o assassino de tempo em ação.
O post Como acelerar expressões regulares sob pressão de produção apareceu pela primeira vez no The New Stack.