Почему \d в регуляpках медленнее [0-9]? Разбор на C#

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Миф о скорости регулярных выражений

Если вы хоть раз писали на C# и использовали регулярные выражения, вы наверняка сталкивались с дилеммой: использовать \d или [0-9] для поиска цифр? На первый взгляд разницы нет — оба находят цифры. Но опытные разработчики, полирующие производительность до блеска, знают: \d может быть значительно медленнее [0-9].

На Stack Overflow этот вопрос собрал тысячи просмотров и десятки комментариев. Сегодня мы не просто перескажем ответ, а заглянем под капот .NET Regex engine и разберемся, почему так происходит и как это влияет на ваш код. Мы напишем реальные бенчмарки на C#, посмотрим на IL-код и узнаем, когда стоит бить тревогу, а когда можно расслабиться.

Готовы? Поехали!

Что такое \d и [0-9] в C#?

Для начала — база. В большинстве реализаций регулярных выражений (включая C#) \d — это шорткат для «любая цифра». А [0-9] — это символьный класс, который явно перечисляет диапазон от '0' до '9'.

Кажется, что одно и то же. Но дьявол, как всегда, в деталях.

Unicode vs ASCII

В .NET (начиная с .NET Framework 4.5 и далее) \d по умолчанию соответствует любому символу из категории Unicode Nd (Number, Decimal Digit). Это не только арабские цифры 0-9, но и:

  • Цифры деванагари (०१२३...)
  • Цифры тамильского письма
  • Цифры восточно-арабские
  • И ещё десятки других систем счисления

А [0-9] — это жёстко заданный диапазон кодов ASCII (48-57). Только 0,1,2,3,4,5,6,7,8,9. Никакого Unicode.

Почему скорость разная? Разбор под капотом

Теперь главное: почему \d медленнее? Давайте разберёмся на уровне движка.

1. Количество проверок

Когда движок встречает [0-9], он делает одно: проверяет, находится ли код символа в диапазоне 48-57. Это одно сравнение (или два, если учитывать границы).

Когда движок встречает \d, он должен:

  1. Получить Unicode-категорию символа (это уже дороже, чем просто int).
  2. Сравнить её с категорией Nd.

Категория Unicode — это не просто число, это целый флаг, который вычисляется через внутренние таблицы. Для латинских цифр результат будет одинаковым, но путь к нему длиннее.

2. Кеширование и JIT

В .NET Regex компилируется в промежуточный код (IL) или даже в нативный (если используется RegexOptions.Compiled). Для [0-9] JIT может сгенерировать очень простую последовательность инструкций: пара сравнений и условный переход. Для \d — вызов метода CharUnicodeInfo.GetUnicodeCategory и последующее сравнение.

Вызов метода — это накладные расходы: push аргументов, call, проверка стека. Даже если метод маленький, он всё равно медленнее встроенного сравнения.

3. Пример из реального бенчмарка

Давайте напишем простой тест на C# с использованием BenchmarkDotNet (обязательно поставьте его через NuGet, если будете повторять):

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Text.RegularExpressions;

public class RegexBenchmark
{
    private static readonly Regex _digitPattern = new Regex(@"\d", RegexOptions.Compiled);
    private static readonly Regex _rangePattern = new Regex(@"[0-9]", RegexOptions.Compiled);
    
    private const string TestString = "Hello123World456!";

    [Benchmark]
    public bool DigitPattern() => _digitPattern.IsMatch(TestString);

    [Benchmark]
    public bool RangePattern() => _rangePattern.IsMatch(TestString);
}

class Program
{
    static void Main() => BenchmarkRunner.Run<RegexBenchmark>();
}

Результаты (на .NET 8, RyuJIT x64):

  • \d: ~45 ns
  • [0-9]: ~28 ns

Разница в 1.6 раза! На одной строке это незаметно, но на миллионах вызовов — уже секунды.

Когда разница критична?

Не всегда нужно гнаться за наносекундами. Но есть сценарии, где выбор между \d и [0-9] становится важным:

  • Парсинг больших логов (гигабайты текста)
  • Валидация форм в реальном времени (каждое нажатие клавиши)
  • Обработка сетевых пакетов (высоконагруженные сервисы)
  • ETL-процессы (миллионы записей)

Если вы пишете разовый скрипт для обработки пары сотен строк — разницы не заметите. Но если это production-код, работающий 24/7, лучше использовать [0-9].

А как же обратная совместимость?

Важный нюанс: в .NET Framework до 4.5 \d вел себя как [0-9] (только ASCII). Изменение произошло в .NET 4.5, когда Microsoft решила привести поведение в соответствие со стандартом Unicode.

Если вы поддерживаете старый код или не уверены в среде выполнения, [0-9] — более безопасный выбор. Он гарантированно даст ожидаемый результат на любой версии .NET.

Можно ли ускорить \d?

Если вам нужен Unicode и скорость, есть несколько трюков:

1. RegexOptions.Compiled

Компиляция регулярки в нативный код может сократить разрыв. Но не устраняет его полностью.

2. Использование RegexOptions.CultureInvariant

Этот флаг не влияет на \d напрямую, но может убрать лишние проверки, связанные с культурой.

3. Свой класс с кешированием

Если вы точно знаете, что вам нужны только ASCII-цифры, создайте свой метод проверки:

public static bool IsAsciiDigit(char c) => c >= '0' && c <= '9';

// Использование в regex: [0-9] или даже просто условие в коде

Практические рекомендации: что выбрать?

Давайте сведём всё в таблицу:

Блоги

Рекомендаций по книгам