Introdução: O mito da velocidade das expressões regulares
\n\nSe você já escreveu em C# e usou expressões regulares, certamente já enfrentou o dilema: usar \\d ou [0-9] para buscar dígitos? À primeira vista, não há diferença — ambos encontram dígitos. Mas desenvolvedores experientes, que polim o desempenho até o brilho, sabem: \\d pode ser significativamente mais lento que [0-9].
No Stack Overflow, essa pergunta acumulou milhares de visualizações e dezenas de comentários. Hoje, não vamos apenas recontar a resposta, mas vamos olhar sob o capô do motor .NET Regex e entender por que isso acontece e como isso afeta seu código. Escreveremos benchmarks reais em C#, analisaremos o código IL e descobriremos quando é hora de se preocupar e quando podemos relaxar.
\n\nProntos? Vamos lá!
\n\nO que são \\d e [0-9] em C#?
\n\nPara começar — o básico. Na maioria das implementações de expressões regulares (incluindo C#), \\d é um atalho para "qualquer dígito". E [0-9] é uma classe de caracteres que lista explicitamente o intervalo de '0' a '9'.
Parece a mesma coisa. Mas o diabo, como sempre, está nos detalhes.
\n\nUnicode vs ASCII
\n\nNo .NET (a partir do .NET Framework 4.5 em diante), \\d por padrão corresponde a qualquer caractere da categoria Unicode Nd (Number, Decimal Digit). Isso não inclui apenas os dígitos arábicos 0-9, mas também:
- \n
- Dígitos devanágari (०१२३...) \n
- Dígitos da escrita tâmil \n
- Dígitos arábico-orientais \n
- E dezenas de outros sistemas numéricos \n
Já [0-9] é um intervalo rigidamente definido de códigos ASCII (48-57). Apenas 0,1,2,3,4,5,6,7,8,9. Nada de Unicode.
Por que a velocidade é diferente? Análise interna
\n\nAgora o principal: por que \\d é mais lento? Vamos entender no nível do motor.
1. Número de verificações
\n\nQuando o motor encontra [0-9], ele faz uma coisa: verifica se o código do caractere está no intervalo 48-57. Isso é uma comparação (ou duas, considerando os limites).
Quando o motor encontra \\d, ele precisa:
- \n
- Obter a categoria Unicode do caractere (isso já é mais caro do que apenas um int). \n
- Compará-la com a categoria
Nd. \n
A categoria Unicode não é apenas um número, é uma bandeira inteira que é calculada por meio de tabelas internas. Para dígitos latinos, o resultado será o mesmo, mas o caminho até ele é mais longo.
\n\n2. Cache e JIT
\n\nNo .NET, o Regex é compilado para código intermediário (IL) ou até mesmo nativo (se for usado RegexOptions.Compiled). Para [0-9], o JIT pode gerar uma sequência muito simples de instruções: um par de comparações e um desvio condicional. Para \\d — uma chamada ao método CharUnicodeInfo.GetUnicodeCategory e uma comparação subsequente.
Chamar um método tem custos indiretos: push de argumentos, call, verificação de pilha. Mesmo que o método seja pequeno, ele ainda é mais lento que uma comparação embutida.
\n\n3. Exemplo de um benchmark real
\n\nVamos escrever um teste simples em C# usando BenchmarkDotNet (certifique-se de instalá-lo via NuGet se for repetir):
\n\nusing BenchmarkDotNet.Attributes;\nusing BenchmarkDotNet.Running;\nusing System.Text.RegularExpressions;\n\npublic class RegexBenchmark\n{\n private static readonly Regex _digitPattern = new Regex(@"\\d", RegexOptions.Compiled);\n private static readonly Regex _rangePattern = new Regex(@"[0-9]", RegexOptions.Compiled);\n \n private const string TestString = "Hello123World456!";\n\n [Benchmark]\n public bool DigitPattern() => _digitPattern.IsMatch(TestString);\n\n [Benchmark]\n public bool RangePattern() => _rangePattern.IsMatch(TestString);\n}\n\nclass Program\n{\n static void Main() => BenchmarkRunner.Run<RegexBenchmark>();\n}\n\nResultados (no .NET