Skip to content

Repository files navigation

RegexProof

Замеры регулярных выражений в .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 раз больше самого создания, в зависимости от паттерна.


Кэш статического Regex.IsMatch

Кэш держит 15 паттернов, шестнадцатый вытесняет первый. С RegexOptions.Compiled каждый промах кэша собирает IL и компилирует его заново.

Кэш статического IsMatch


Два вида промаха

Совпадения нет ни в одной строке, длина одна и та же, а разница — до трёх порядков. Всё решает поиск возможной стартовой позиции.

Два вида промаха


Первое обращение после запуска процесса

Первое обращение


Вложенный повтор

Известный пример ^(?: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

Ссылки

About

Regex в .NET 8/9/10: GeneratedRegex, Compiled, интерпретатор, NonBacktracking и статический IsMatch — замеры на четырёх машинах с разбором сгенерированного кода

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages