Уважаемые читатели, в этой статье я хочу разобраться, в какой момент .NET компилирует регулярное выражение — в конструкторе или при первом вызове, — и представить свои выводы.
Проверить строку регулярным выражением в .NET можно по-разному, но совет всегда один: включите RegexOptions.Compiled или возьмите генератор. Замеры это подтверждают. А вот подготовка самого выражения оказалась не там, где её меряют: конструктор занимает 10 микросекунд, а первая проверка на этом объекте — ещё 1307.
Сравниваются шесть способов:
new Regex — разбирает паттерн при создании и дальше работает интерпретатором;
RegexOptions.Compiled — собирает под паттерн IL-метод во время работы приложения;
[GeneratedRegex] — генератор пишет обычный C# код при сборке проекта;
статический Regex.IsMatch — паттерн передаётся строкой в каждый вызов;
RegexOptions.NonBacktracking — движок, который не возвращается назад по строке;
string.Contains — обычный поиск подстроки, без регулярного выражения.
Будет 3 истории:
что не попадает в замер создания;
генератор против Compiled на проверке строки — почему два промаха на строках одной длины отличаются в тысячу раз;
что написал генератор — и что стало с классическим примером про опасность регулярных выражений.
Весь код проверки — в репозитории RegexProof. Замеры сделаны на четырёх машинах:
№ |
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 |
BenchmarkDotNet 0.15.8, сборка Release, рантаймы .NET 8, 9 и 10. Патч-версии на машинах разные, поэтому сравнивать имеет смысл столбцы внутри одной машины, а не числа с разных машин.
Все таблицы ниже — по .NET 10. У Compiled и генератора на восьмёрке и девятке те же числа в пределах нескольких процентов. У интерпретатора и RegexOptions.NonBacktracking разброс больше и зависит от машины: на №1 разницы между рантаймами нет, а на №4 они на .NET 8 медленнее почти вдвое. Полные отчёты по всем трём рантаймам лежат в репозитории.
Паттернов три:
// RegexProof, Subjects.cs // точная подстрока public const string LiteralPattern = "orders/export"; // разбор адреса электронной почты public const string EmailPattern = @"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"; // повторы и варианты public const string HeavyPattern = @"(?:[a-z]+\d+){2,}(?:foo|bar|baz)+end";
История 1. Конструктор показал 10 мкс, первый вызов — 1307
Сначала посмотрим, сколько занимает получение готового объекта. Паттерн для адреса электронной почты, .NET 10.
Способ |
№1 Ryzen, нс |
№2 i9, нс |
№3 Xeon Silver, нс |
№4 Xeon W, нс |
Выделено |
new Regex |
1 402 |
1 330 |
2 185 |
1 953 |
2 736 Б |
new Regex + Compiled |
9 503 |
10 055 |
14 822 |
13 501 |
14 024 Б |
new Regex + NonBacktracking |
53 798 |
80 079 |
99 201 |
121 907 |
198 176 Б |
[GeneratedRegex] |
1 |
1 |
2 |
1 |
0 Б |
Получение готового объекта, .NET 10: в прогретом коде обращение к методу генератора занимает наносекунду и не выделяет памяти, потому что объект уже создан
Читается таблица так:
new Regex разбирает паттерн и строит внутреннее представление — от полутора до двух с небольшим микросекунд и пара килобайт;
с RegexOptions.Compiled к этому добавляется сборка IL-метода под конкретный паттерн, отсюда и 14 килобайт;
RegexOptions.NonBacktracking строит из паттерна автомат, и на него уходит 198 килобайт и от 54 до 122 микросекунд;
[GeneratedRegex] не строит ничего: код написан при сборке проекта, объект создаётся один раз при первом обращении, а дальше метод отдаёт его из статического поля.
Последнее видно в исходнике, который написал генератор, — он лежит в папке Generated:
// Generated/System.Text.RegularExpressions.Generator/.../RegexGenerator.g.cs public static partial global::System.Text.RegularExpressions.Regex EmailGenerated() => global::System.Text.RegularExpressions.Generated.EmailGenerated_1.Instance;
Теперь про кэш. У статических методов Regex есть общий кэш разобранных паттернов, его размер задаёт свойство Regex.CacheSize. В документации сказано прямым текстом: по умолчанию кэш хранит пятнадцать паттернов. Пока их не больше, каждый вызов берёт из кэша готовый объект. Шестнадцатый вытесняет первый.
Замер перебирает границу: 1, 8 и 15 паттернов помещаются в кэш, 16 и 32 — уже нет.

Зелёные столбики — контроль. Те же паттерны, но объекты созданы заранее и лежат в поле: 29–35 наносекунд, и число паттернов на результат не влияет. Значит дело именно в кэше.
Группу с одним паттерном сравнивать с остальными не надо: там каждый вызов заканчивается совпадением, а дальше совпадает только один вызов из N. Сопоставимы группы от восьми и правее.
Граница воспроизводится на всех четырёх машинах.
Машина |
IsMatch, 15 |
IsMatch, 16 |
IsMatch + Compiled, 15 |
IsMatch + Compiled, 16 |
№1 Ryzen |
67 нс |
1 490 нс |
68 нс |
456 566 нс |
№2 i9 |
76 нс |
1 416 нс |
81 нс |
471 368 нс |
№3 Xeon Silver |
118 нс |
2 563 нс |
119 нс |
654 996 нс |
№4 Xeon W |
100 нс |
2 192 нс |
105 нс |
600 756 нс |
Переход от пятнадцати паттернов к шестнадцати, .NET 10: обычная перегрузка замедляется в 19–22 раза, перегрузка с Compiled — в 5 483–6 723 раза
С обычной перегрузкой всё сходится: промах кэша даёт 1 490 наносекунд и 3 528 байт, а создание объекта из первой таблицы — 1 402 наносекунды и 2 736 байт. Разница в пределах того, что дают разные паттерны, то есть при промахе просто строится новый объект.
А с Compiled числа не сходятся. Промах кэша выделяет 15 014 байт против 14 024 у одного создания объекта, то есть объект тоже построен один раз. А времени уходит 456 микросекунд вместо 9,5.
Что меряется |
Время |
Выделено |
new Regex(pattern, Compiled) |
9 503 нс |
14 024 Б |
Промах кэша: создание и одна проверка |
456 566 нс |
15 014 Б |
Машина №1, .NET 10: памяти выделяется почти одинаково, времени — в 48 раз больше
Значит время уходит не на создание объекта. Проверим напрямую: замерим отдельно создание и создание вместе с одной проверкой. Паттерн тот же, адрес электронной почты.
Что меряется |
№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 |
.NET 10: у интерпретатора первая проверка добавляет 365–720 наносекунд, у Compiled — от 1,3 до 1,9 миллисекунды
У интерпретатора первая проверка добавляет 365–720 наносекунд — примерно столько же, сколько такая проверка занимает на прогретом объекте во второй истории. У Compiled та же проверка добавляет от 1,3 до 1,9 миллисекунды, то есть в 118–141 раз больше самого создания. На точной подстроке разрыв в 117–130 раз, на тяжёлом паттерне — в 217–231. Так на всех четырёх машинах.
Причина в том, как устроен RegexOptions.Compiled, и это описано в документации: конструктор превращает выражение в IL и складывает его в объекты DynamicMethod, а дальше этот IL компилирует JIT — уже при первом вызове. Тем же занимается RegexCompiler.cs в исходниках рантайма. Замер создания видит только первую часть работы, а вторая, которая в сто с лишним раз больше, приходит с первой проверкой.
Собранный метод попадает в дизассемблированный листинг наравне с обычными методами сборки:
; Disasm/Listings_Comp_2/disasm_net10.txt ; System.Text.RegularExpressions.CompiledRegexRunner: ; Regex1_TryFindNextPossibleStartingPosition(RegexRunner, ReadOnlySpan<char>):bool (FullOpts) lea rcx, bword ptr [r8+2*rcx] mov edx, edi sub edx, esi call [System.Buffers.IndexOfAnyAsciiSearcher:IndexOfAnyCore[...]] test eax, eax jl SHORT G_M000_IG06 ; Total bytes of code 120
Пометка FullOpts в заголовке листинга означает, что JIT сразу собрал полностью оптимизированный код, без промежуточного быстрого уровня. Для сравнения, у генератора в том же листинге стоит Tier0: его код проходит обычные уровни компиляции, как любой другой метод сборки. Отсюда и разница в первом вызове.
Заодно видно, чем занят поиск стартовой позиции: в этом листинге за него отвечает IndexOfAnyAsciiSearcher из System.Buffers — векторный поиск сразу по набору символов. Набор здесь произвольный, [A-Za-z0-9._%+-]. Для непрерывного диапазона вроде [a-z] берётся вариант попроще, IndexOfAnyInRange, — он встретится в третьей истории.
Отсюда практический вывод. Документация описывает кэш и его размер, но про промах с RegexOptions.Compiled там ничего нет. А происходит вот что: больше пятнадцати разных паттернов через статический Regex.IsMatch с этим флагом — и каждый вызов собирает IL и компилирует его заново.
Тот же эффект виден и на старте приложения. BenchmarkDotNet для такого замера не подходит: он прогревает код, а вопрос как раз в первом вызове. Поэтому замер сделан отдельно: Stopwatch запускается внутри Main, то есть меряется не старт процесса, а первое обращение к регулярному выражению после него. Каждый способ идёт в своём процессе, по десять запусков.

На всех четырёх машинах порядок один и тот же. Приложению, которое работает сутками, эти миллисекунды безразличны. А для консольной утилиты или функции, которая стартует на каждый запрос, они заметны.
История 2. Проверка строки: генератор против Compiled и два разных промаха
Дальше — прогретый код. Замеры идут по трём паттернам, двум длинам строки (100 и 10 000 символов) и четырём вариантам самой строки:
совпадение в начале строки;
совпадение в конце;
совпадения нет, и символов, с которых паттерн может начаться, в строке тоже нет;
совпадения нет, но такие символы стоят на каждом шагу.
Начнём с адреса электронной почты в конце строки на 10 000 символов.
Способ |
№1 Ryzen, нс |
№2 i9, нс |
№3 Xeon Silver, нс |
№4 Xeon W, нс |
new Regex |
8 926 |
10 053 |
16 194 |
12 869 |
new Regex + Compiled |
246 |
302 |
497 |
398 |
[GeneratedRegex] |
227 |
295 |
482 |
389 |
статический Regex.IsMatch |
8 916 |
10 051 |
14 645 |
13 418 |
new Regex + NonBacktracking |
8 859 |
10 006 |
15 304 |
12 890 |
string.Contains |
291 |
455 |
466 |
593 |
Совпадение в конце строки на 10 000 символов, .NET 10: Compiled и генератор дают одинаковый результат и обгоняют интерпретатор в 32–39 раз
Отсюда три наблюдения:
Генератор и Compiled неразличимы. По всем 48-ми сочетаниям паттерна, варианта строки и машины отношение генератора к Compiled укладывается в 0,78–1,15 при медиане 0,96. Код у них разный, но оба уходят от интерпретатора к машинному коду, просто разными путями;
Статический Regex.IsMatch идёт наравне с обычным new Regex. Кэш экономит создание объекта, но в перегрузке без RegexOptions этот объект интерпретируемый — там просто негде указать Compiled. Поэтому сама проверка быстрее не становится. С таблицей из первой истории эти числа не сравниваются: там строка была короткая и мерился кэш, здесь строка на десять тысяч символов и мерится сама проверка;
RegexOptions.NonBacktracking держится на уровне интерпретатора и отстаёт от Compiled в 31–36 раз. Это на адресе: на двух других паттернах разницы с Compiled нет. В третьей истории будет видно, где он выходит вперёд.
Теперь уберём совпадение из строки. Сделаем это двумя разными способами:
в первой строке нет ни одного символа, с которого мог бы начаться адрес;
во второй такие символы стоят на каждом шагу, но адреса в ней всё равно нет.
Паттерн и строка |
№1 Ryzen, нс |
№2 i9, нс |
№3 Xeon Silver, нс |
№4 Xeon W, нс |
Адрес, символов из набора нет |
210 |
273 |
440 |
367 |
Адрес, символы из набора на каждом шагу |
11 060 |
12 512 |
23 246 |
18 141 |
Тяжёлый паттерн, символов нет |
185 |
220 |
223 |
329 |
Тяжёлый паттерн, символы на каждом шагу |
120 316 |
135 745 |
244 016 |
189 733 |
RegexOptions.Compiled, 10 000 символов, совпадения нет, .NET 10: у адреса разница в 46–53 раза, у тяжёлого паттерна — в 577–1 094 раза
Строки одинаковой длины, совпадения нет ни в одной, а разница доходит до трёх порядков. Дело в том, что перед запуском движка выполняется поиск возможной стартовой позиции.
Если подходящих символов в строке нет, поиск уходит в IndexOfAnyAsciiSearcher — тот самый вызов из листинга в первой истории — и проверяет по несколько символов за раз. Если такие символы есть, движок запускается на каждом из них и каждый раз упирается в несовпадение.
Поэтому «промах отрабатывает за наносекунды» верно только для первого случая. В кэше, где промахи выглядят так же, как попадания, работает второй.
На длинах до миллиона символов обе зависимости остаются линейными, отличается только множитель.

И последнее в этой истории. Всё выше замерялось методом IsMatch. Он останавливается на первом совпадении и не собирает ни границы групп, ни сами подстроки, поэтому работы у него меньше, чем у остальных методов.
Метод |
№1 Ryzen, нс |
№2 i9, нс |
№3 Xeon Silver, нс |
№4 Xeon W, нс |
Выделено |
IsMatch |
43 |
51 |
84 |
63 |
0 Б |
Match |
58 |
75 |
119 |
102 |
208 Б |
EnumerateMatches |
3 452 |
5 048 |
8 668 |
6 041 |
0 Б |
EnumerateMatches, только счёт |
3 697 |
5 000 |
7 903 |
6 018 |
0 Б |
Count(string) |
4 864 |
4 367 |
7 226 |
5 952 |
0 Б |
Count(ReadOnlySpan<char>) |
3 608 |
4 071 |
7 021 |
4 993 |
0 Б |
Replace |
4 256 |
5 071 |
8 451 |
6 195 |
824 Б |
Matches |
7 456 |
8 233 |
13 843 |
11 914 |
23 080 Б |
Сто совпадений в строке, .NET 10: IsMatch и Match останавливаются на первом совпадении, поэтому их числа не сравнимы с остальными строками
IsMatch и Match до конца строки не доходят, так что сравнивать между собой имеет смысл шесть нижних строк. Главное там — Matches: он единственный выделяет память, 23 килобайта на сотню совпадений, и работает в 1,6–2,2 раза дольше EnumerateMatches.
Остальные четыре способа обходят строку без единого выделенного байта и укладываются в один диапазон. Единственная устойчивая разница между ними — перегрузка Count: со строкой она на 3–35% медленнее, чем со span, на всех четырёх машинах. А обход с обращением к длине совпадения и без него не отличается.
И ещё одно, что видно по таблице: EnumerateMatches отдаёт только границы совпадений. Групп и захваченных подстрок от него не получить — если они нужны, придётся брать Matches со всеми его аллокациями.
История 3. Что написал генератор
У генератора есть свойство, которого нет у Compiled: его результат можно открыть и прочитать. По умолчанию этот файл на диск не попадает, но включается он двумя строками в файле проекта:
<!-- RegexProof.csproj --> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
После сборки в папке Generated лежит обычный C# код. Вот что получилось для точной подстроки:
// Generated/.../RegexGenerator.g.cs, паттерн "orders/export" protected override void Scan(ReadOnlySpan<char> inputSpan) { if (TryFindNextPossibleStartingPosition(inputSpan)) { // The search in TryFindNextPossibleStartingPosition performed the entire match. int start = base.runtextpos; int end = base.runtextpos = start + 13; base.Capture(0, start, end); } }
Проверки совпадения здесь нет: поиск стартовой позиции сразу находит всю подстроку, и разбор регулярного выражения не выполняется. Генератор написал об этом отдельный комментарий прямо в коде.
А вот поиск стартовой позиции для тяжёлого паттерна:
// Generated/.../RegexGenerator.g.cs, паттерн (?:[a-z]+\d+){2,}(?:foo|bar|baz)+end private bool TryFindNextPossibleStartingPosition(ReadOnlySpan<char> inputSpan) { int pos = base.runtextpos; // Any possible match is at least 10 characters. if (pos <= inputSpan.Length - 10) { // The pattern begins with a character in the set [a-z]. int i = inputSpan.Slice(pos).IndexOfAnyInRange('a', 'z'); if (i >= 0) { base.runtextpos = pos + i; return true; } } base.runtextpos = inputSpan.Length; return false; }
Вот и объяснение тем 185–329 наносекундам на десяти тысячах символов из второй истории. Разбор выражения тут вообще не выполняется: всё делает один вызов IndexOfAnyInRange, который проверяет по несколько символов за раз. Но стоит в строке появиться строчным буквам, и эта проверка начинает возвращать true на каждом шаге. Дальше берётся за работу сам разбор — отсюда и разница в тысячу раз.
Теперь про возвраты. Есть известный пример, которым принято показывать, чем опасны регулярные выражения: паттерн с вложенным повтором одного и того же символа и строка, которая ему почти подходит. Каждый лишний символ должен удваивать число вариантов разбора, и на двадцати с небольшим символах проверка должна занимать минуты.
// RegexProof, Subjects.cs public const string NestedPattern = "^(?:a+)+$"; // строка: Length символов "a" и один "X" в конце - совпадения нет
Замер на строках от 16 до 22 символов, машина №1, .NET 10.

Удвоения не происходит, и ответ снова в сгенерированном исходнике:
// Generated/.../RegexGenerator.g.cs, паттерн ^(?:a+)+$ /// Explanation: /// ○ Match if at the beginning of the string. /// ○ Match 'a' atomically at least once. /// ○ Match if at the end of the string or if before an ending newline. // и сам разбор - один вызов вместо перебора вариантов: int iteration = slice.IndexOfAnyExcept('a');
Вложенный повтор свернулся в один атомарный. Атомарный означает, что движок не станет возвращаться и отдавать назад уже прочитанные символы, а перебор вариантов строился именно на этих возвратах.
Вместо него остался обычный поиск первого символа, отличного от «a»: время у него тоже растёт с длиной строки, но линейно, а не вдвое на каждый символ. Свернул повтор не генератор, а разбор паттерна, общий для всех вариантов, — на графике выше интерпретатор ведёт себя так же. Появилось это в .NET 7, Stephen Toub разбирает такие упрощения паттернов в статье Regular Expression Improvements in .NET 7.
Но возвраты никуда не делись. Вернёмся к тяжёлому паттерну и строке, где символы из его набора встречаются на каждом шагу, — и посмотрим на RegexOptions.NonBacktracking, который во второй истории держался на уровне интерпретатора.
Способ |
№1 Ryzen, нс |
№2 i9, нс |
№3 Xeon Silver, нс |
№4 Xeon W, нс |
new Regex |
595 096 |
580 309 |
910 933 |
958 761 |
new Regex + Compiled |
120 316 |
135 745 |
244 016 |
189 733 |
[GeneratedRegex] |
101 020 |
123 831 |
223 948 |
168 051 |
new Regex + NonBacktracking |
33 123 |
35 762 |
56 672 |
48 395 |
Тяжёлый паттерн, 10 000 символов, совпадения нет, но символы из набора стоят на каждом шагу, .NET 10: RegexOptions.NonBacktracking быстрее скомпилированного в 3,6–4,3 раза
Порядок мест поменялся. Интерпретатор тратит почти миллисекунду на десять тысяч символов, Compiled — в 3,7–5,1 раза меньше, а RegexOptions.NonBacktracking обгоняет Compiled в 3,6–4,3 раза, хотя на обычных строках отставал в тридцать с лишним. Он проходит строку один раз и не перебирает варианты — там, где вариантов много, это и решает.
Выводы
По цифрам:
обращение к методу с [GeneratedRegex] в прогретом коде занимает 1–2 наносекунды и не выделяет памяти: код написан при сборке проекта, объект создаётся один раз при первом обращении;
new Regex с RegexOptions.Compiled для адреса электронной почты занимает 9,5–14,8 мкс и 14 КБ, но это не вся работа: первая проверка на только что созданном объекте добавляет ещё 1,3–1,9 мс. По трём паттернам и четырём машинам это в 117–231 раз больше самого создания;
статический Regex.IsMatch держит кэш на 15 паттернов. Шестнадцатый паттерн замедляет вызов в 19–22 раза, а с RegexOptions.Compiled — в 5 483–6 723 раза на всех четырёх машинах;
на прогретом коде [GeneratedRegex] и RegexOptions.Compiled неразличимы: 0,78–1,15 при медиане 0,96 по 48-ми замерам;
первое обращение после запуска процесса: интерпретатор 4,6–6,7 мс, генератор 9,0–15,0 мс, Compiled 18,0–29,3 мс;
два промаха на строках одной длины отличаются в разы: если символов из набора в строке нет, проверка занимает 185–440 нс на 10 000 символов, если есть — в 46–53 раза дольше у адреса и в 577–1 094 раза у тяжёлого паттерна;
RegexOptions.NonBacktracking сильно зависит от паттерна: на адресе электронной почты он отстаёт от Compiled в 31–36 раз, на точной подстроке в 1,1–1,5 раза, а на тяжёлом паттерне разницы между ними нет. Зато на строке, где паттерн начинает совпадать на каждом шагу и каждый раз обрывается, обгоняет в 3,6–4,3 раза;
вложенный повтор ^(?:a+)+$ разбирается за наносекунды: разбор паттерна сворачивает его в атомарный, и это работает во всех вариантах, включая интерпретатор. Роста вдвое на каждый символ нет, остаётся линейный проход;
Matches — единственный способ обойти все совпадения, который выделяет память: 23 080 байт на сотню совпадений, и он в 1,6–2,2 раза дольше EnumerateMatches. Count, Replace и EnumerateMatches обходятся без аллокаций.
Что делать на практике:
если паттерн известен на этапе компиляции, лучший вариант — [GeneratedRegex]. Проверяет он с той же скоростью, что Compiled, но ничего не собирает после запуска приложения, отдаёт готовый объект и работает под NativeAOT, где сборка IL во время работы недоступна: в Regex.cs компиляция включается только при RuntimeFeature.IsDynamicCodeCompiled, иначе создаётся интерпретатор. То же самое советует и документация. Плюс сгенерированный код можно открыть и посмотреть, что получилось;
если паттерн становится известен только после запуска, подойдёт new Regex с RegexOptions.Compiled — но объект имеет смысл создать один раз и держать в статическом поле, иначе за него придётся расплачиваться на каждом первом вызове;
статический
Regex.IsMatchудобен, пока разных паттернов в приложении не больше пятнадцати — это размер кэша по умолчанию, он задаётся черезRegex.CacheSize. Дальше кэш начинает вытеснять паттерны, а потому Regex лучше создать заранее и держать в поле;RegexOptions.Compiled в статическом Regex.IsMatch — сочетание, которого лучше избегать: при промахе кэша каждый вызов собирает IL и компилирует его заново, а это сотни микросекунд;
если искать фиксированную подстроку, регулярное выражение не даёт выигрыша: у string.Contains и скомпилированной регулярки результаты почти одинаковые, на двух машинах из четырёх Contains быстрее, на двух медленнее;
если во входных строках часто встречаются символы, с которых паттерн может начаться, есть смысл померить
RegexOptions.NonBacktracking: на обычных данных он отстаёт, а на таких обгоняет остальных. Везде его поставить не получится — обратных ссылок и просмотра назад он не поддерживает;если нужно обойти все совпадения, а сами объекты Match не нужны, подойдёт любой из способов без аллокаций — EnumerateMatches, Count или Replace. Matches стоит брать, только если нужны сами объекты: он вдвое медленнее и выделяет 23 килобайта на сотню совпадений;
для короткоживущих процессов первое обращение важнее прогретой скорости, а там RegexOptions.Compiled медленнее интерпретатора в 3,7–5,1 раза.
Код из статьи
RegexProof — бенчмарки, прогоны на четырёх машинах, сгенерированный исходник и листинги машинного кода
Ссылки
Всем удачи и до новых встреч!