Почему \d в C# работает медленнее, чем [0-9], и как это исправить?
Почему \d медленнее [0-9] в C#?
Ключевая причина — различие в семантике этих токенов в .NET. Вопреки распространённому мнению, \d не эквивалентен [0-9] в C#. \d соответствует любой цифре из категории Юникода Nd (Number, Decimal Digit), которая включает не только арабские цифры 0-9, но и цифры из других систем счисления: арабо-индийские, восточно-арабские, некоторые китайские иероглифы и т.д. В то время как [0-9] — это строгий класс, ограниченный ASCII-символами от '0' до '9'.
Влияние на производительность
Разница в скорости обусловлена двумя факторами:
- Проверка категории Юникода: Для
\dдвижок Regex должен для каждого символа выполнить дополнительную операцию — проверить, принадлежит ли символ категорииNd. Это требует обращения к таблицам Юникода, что значительно дороже простого сравнения диапазона ASCII. - Оптимизация JIT: Класс
[0-9]— это простой диапазон, который JIT-компилятор может легко оптимизировать в несколько ассемблерных инструкций. Для\dтакая оптимизация невозможна из-за сложной логики проверки.
Как это исправить?
Есть несколько решений в зависимости от ваших потребностей:
1. Используйте [0-9] если работаете только с ASCII
Если вы точно знаете, что входные данные содержат только латинские цифры, используйте [0-9]. Это самый быстрый и предсказуемый вариант.
// Быстро и безопасно для ASCII
string pattern = @"[0-9]+";
2. Включите режим ECMAScript
В .NET можно переключить движок Regex в режим совместимости с ECMAScript. В этом режиме \d становится эквивалентен [0-9] и работает с той же скоростью.
// Включаем ECMAScript через опцию
var regex = new Regex(@"\d+", RegexOptions.ECMAScript);
// Или через inline-флаг (?-n)
var match = Regex.Match(input, @"(?n)\d+");
3. Используйте скомпилированные регулярные выражения
Для многократного использования одного и того же шаблона создавайте экземпляр Regex с опцией RegexOptions.Compiled. Это ускорит выполнение за счёт предварительной компиляции в MSIL.
// Компилируем регулярку один раз
var regex = new Regex(@"\d+", RegexOptions.Compiled);
for (int i = 0; i < 1000; i++)
{
regex.Match(input);
}
Практический пример с замерами
Вот полный тест, демонстрирующий разницу:
using System;
using System.Diagnostics;
using System.Text.RegularExpressions;
class PerformanceTest
{
static void Main()
{
string input = new string('9', 1000000);
int iterations = 5000;
// Тест 1: \d без опций
var sw = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
Regex.Match(input, @"\d+");
Console.WriteLine($"\\d: {sw.ElapsedMilliseconds} ms");
// Тест 2: [0-9]
sw.Restart();
for (int i = 0; i < iterations; i++)
Regex.Match(input, @"[0-9]+");
Console.WriteLine($"[0-9]: {sw.ElapsedMilliseconds} ms");
// Тест 3: \d с ECMAScript
sw.Restart();
for (int i = 0; i < iterations; i++)
Regex.Match(input, @"\d+", RegexOptions.ECMAScript);
Console.WriteLine($"\\d (ECMAScript): {sw.ElapsedMilliseconds} ms");
// Тест 4: скомпилированный \d
var compiledRegex = new Regex(@"\d+", RegexOptions.Compiled);
sw.Restart();
for (int i = 0; i < iterations; i++)
compiledRegex.Match(input);
Console.WriteLine($"\\d (Compiled): {sw.ElapsedMilliseconds} ms");
}
}
Итоговая рекомендация
- Если данные гарантированно ASCII — используйте
[0-9]. Это самый быстрый вариант. - Если нужна поддержка Юникода — используйте
\dс опциейRegexOptions.Compiledдля многократного использования. - Если нужна максимальная скорость и совместимость с другими языками — включите
RegexOptions.ECMAScript, но помните, что это отключает некоторые возможности .NET Regex.
Всегда профилируйте свой конкретный сценарий, так как разница может варьироваться в зависимости от размера входных данных и сложности шаблона.
<<Альтернативный подход: использование Span и ручной парсинг
Если производительность критична и вы работаете с большими объёмами данных, рассмотрите возможность отказа от регулярных выражений в пользу ручного парсинга с использованием ReadOnlySpan и MemoryExtensions. Это может дать выигрыш в 5-10 раз по сравнению с любым вариантом Regex.
using System;
public static class DigitParser
{
public static ReadOnlySpan ExtractDigits(ReadOnlySpan input)
{
int start = -1;
for (int i = 0; i < input.Length; i++)
{
if (input[i] >= '0' && input[i] <= '9')
{
if (start == -1) start = i;
}
else if (start != -1)
{
return input.Slice(start, i - start);
}
}
return start == -1 ? ReadOnlySpan.Empty : input.Slice(start);
}
}
// Использование:
string data = "abc123def";
var digits = DigitParser.ExtractDigits(data.AsSpan());
Console.WriteLine(digits.ToString()); // "123"
Этот подход не только быстрее, но и позволяет избежать накладных расходов на создание объектов Match и Group. Однако он требует больше кода и менее гибок для сложных шаблонов. Используйте его для простых задач извлечения цифр, где скорость важнее универсальности.