Замеры регулярных выражений в .NET 8, 9 и 10: new Regex, RegexOptions.Compiled,
[GeneratedRegex], статический Regex.IsMatch, RegexOptions.NonBacktracking
и string.Contains как нижняя граница. Четыре машины, BenchmarkDotNet 0.15.8,
сборка Release.
Репозиторий к статье «Бенчмаркая Regex: конструктор показал 10 мкс, первый вызов — 1307».
new Regex(pattern, RegexOptions.Compiled) собирает IL-метод, но компилирует
этот IL уже JIT — при первом обращении. Поэтому замер создания показывает
не всю работу. Паттерн адреса электронной почты, .NET 10, наносекунды:
| Что меряется | №1 Ryzen | №2 i9 | №3 Xeon Silver | №4 Xeon W |
|---|---|---|---|---|
new Regex |
1 402 | 1 330 | 2 185 | 1 953 |
new Regex и одна проверка |
1 773 | 1 695 | 2 905 | 2 491 |
new Regex + Compiled |
9 503 | 10 055 | 14 822 | 13 501 |
new Regex + Compiled и одна проверка |
1 336 834 | 1 307 369 | 1 879 379 | 1 593 919 |
У интерпретатора первая проверка добавляет 365–720 нс. У Compiled — от 1,3
до 1,9 мс, то есть в 117–231 раз больше самого создания, в зависимости
от паттерна.
Кэш держит 15 паттернов, шестнадцатый вытесняет первый. С RegexOptions.Compiled
каждый промах кэша собирает IL и компилирует его заново.
Совпадения нет ни в одной строке, длина одна и та же, а разница — до трёх порядков. Всё решает поиск возможной стартовой позиции.
Известный пример ^(?:a+)+$ не взрывается: разбор паттерна сворачивает
вложенный повтор в атомарный, и это видно в
сгенерированном исходнике.
Прогретый матч. [GeneratedRegex] и RegexOptions.Compiled неразличимы:
по 48-ми сочетаниям паттерна, входной строки и машины отношение укладывается
в 0,78–1,15 при медиане 0,96. Интерпретатор отстаёт от них в 32–39 раз.
Создание. Обращение к методу с [GeneratedRegex] — 1–2 нс и ноль байт:
объект создаётся один раз при первом обращении, дальше метод отдаёт его
из статического поля. new Regex(..., NonBacktracking) — 54–122 мкс и 198 КБ.
NonBacktracking. Зависит от паттерна: на адресе отстаёт от Compiled
в 31–36 раз, на точной подстроке в 1,1–1,5 раза, на тяжёлом паттерне разницы
нет. Зато на строке, где паттерн начинает совпадать на каждом шагу и каждый
раз обрывается, обгоняет в 3,6–4,3 раза.
Остальные методы. Из тех, что проходят строку целиком, память выделяет
только Matches: 23 080 байт на сотню совпадений и в 1,6–2,2 раза дольше
EnumerateMatches. Перегрузка Count(ReadOnlySpan<char>) на 3–35% быстрее
Count(string) на всех машинах.
| № | CPU | Ядра / потоки | Частота | ОС |
|---|---|---|---|---|
| 1 | AMD Ryzen 9 5950X | 16 / 32 | 3,4 ГГц, до 4,9 в турбо | Windows 10 1809 |
| 2 | Intel Core i9-10900KF | 10 / 20 | 3,7 ГГц, до 5,3 в турбо | Windows 10 22H2 |
| 3 | 2 × Intel Xeon Silver 4314 | 2×16 / 64 | 2,4 ГГц, до 3,4 в турбо | Windows Server 2022 |
| 4 | Intel Xeon W-2255 | 10 / 20 | 3,7 ГГц, до 4,5 в турбо | Windows Server 2022 |
Патч-версии рантайма на машинах разные, поэтому сравнивать имеет смысл столбцы внутри одной машины, а не числа с разных машин.
Нужен .NET SDK 8, 9 и 10 — прогон многоцелевой.
dotnet run -c Release -f net10.0 все замеры
dotnet run -c Release -f net10.0 -- --filter *RegexCreateBench* один класс
cold.bat первое обращение и память
Disasm\snap.bat листинги машинного кода
Отчёты появятся в BenchmarkDotNet.Artifacts\results, первое обращение —
в cold.csv, листинги — рядом со snap.bat.
Исходник, который пишет генератор, сохраняется при каждой сборке в папку
Generated — за это отвечает свойство EmitCompilerGeneratedFiles
в RegexProof.csproj.
Три паттерна: точная подстрока, разбор адреса электронной почты и выражение с повторами и вариантами. Строки на 100 и 10 000 символов, четыре варианта входной строки: совпадение в начале, совпадение в конце, совпадения нет и подходящих символов в строке тоже нет, совпадения нет, но такие символы стоят на каждом шагу.
GlobalSetup сверяет, что все способы дают один ответ и что ответ совпадает
с ожидаемым, — иначе прогон падает.
Каждый измеряемый метод лежит в Subjects.cs и помечен NoInlining:
иначе его не найти в дизассемблированном коде.
Subjects.cs все измеряемые методы
Program.cs запуск замеров
Enums/ параметры бенчмарков
Diagnostics/ первое обращение после запуска и удержанная память
Generated/ исходник, который написал генератор
Benchmarks/
RegexBench.cs матч: 6 способов x 3 паттерна x 4 варианта строки x 2 длины
RegexCreateBench.cs создание объекта и создание вместе с первой проверкой
RegexApiBench.cs Match, Count, Matches, EnumerateMatches, Replace
RegexCacheBench.cs кэш статического IsMatch на границе 15 и 16 паттернов
RegexScaleBench.cs промах на строках от 100 до 1 000 000 символов
RegexBacktrackBench.cs ^(?:a+)+$ на строках 16-22 символа
Results/
Comp_1..Comp_4/ отчёты по каждой машине: csv, md, html и cold.csv
Docs/ графики статьи
Disasm/
Program.cs прогрев для снятия листингов
snap.bat снятие листингов
Listings_Comp_1..4/ листинги по машинам, .NET 8/9/10
- Генератор регулярных выражений — документация
- Compilation and Reuse in Regular Expressions — про IL и JIT
- Regular Expression Improvements in .NET 7
- Regex.CacheSize — размер кэша статических методов
- RegexOptions.NonBacktracking
- Regex.cs — выбор движка при создании
- Regex.Cache.cs — кэш статических методов
- RegexCompiler.cs — сборка IL для RegexOptions.Compiled
- IndexOfAnyAsciiSearcher.cs — векторный поиск по набору символов



