Генераторы источников регулярных выражений .NET
Регулярное выражение или регулярное выражение — это строка, которая позволяет разработчику выразить поиск шаблона, что делает его общим способом поиска текста и извлечения результатов в виде подмножества из строки поиска. В .NET System.Text.RegularExpressions
пространство имен используется для определения Regex экземпляров и статических методов и сопоставления с пользовательскими шаблонами. В этой статье вы узнаете, как использовать создание источников для создания Regex
экземпляров для оптимизации производительности.
Примечание.
По возможности используйте созданные источником регулярные выражения вместо компиляции регулярных выражений RegexOptions.Compiled с помощью параметра. Создание источника может помочь вашему приложению начать быстрее, быстрее запускать и быть более обрезаемым. Чтобы узнать, когда возможно создание источника, см. статью "Когда его использовать".
Скомпилированные регулярные выражения
Когда вы пишете new Regex("somepattern")
, происходит несколько вещей. Указанный шаблон анализируется как для обеспечения допустимости шаблона, так и для преобразования его в внутреннее дерево, представляющее синтаксический ретекс. Затем дерево оптимизировано различными способами, преобразуя шаблон в функционально эквивалентный вариант, который может быть более эффективно выполнен. Дерево записывается в форму, которая может быть интерпретирована как ряд опкодов и операндов, которые предоставляют инструкции обработчику интерпретатора regex по сопоставлению. При выполнении совпадения интерпретатор просто проходит эти инструкции, обрабатывая их с помощью входного текста. При создании экземпляра нового Regex
экземпляра или вызове одного из статических методов Regex
интерпретатор используется подсистемой по умолчанию.
При указании RegexOptions.Compiledвыполняется все одно и то же время строительства. Результирующие инструкции преобразуются компилятором на основе отражения в инструкции IL, записанные в несколько DynamicMethod объектов. При выполнении DynamicMethod
сопоставления эти методы вызываются. Этот IL, по сути, делает именно то, что будет делать интерпретатор, за исключением специализированных для точной обработки шаблона. Например, если шаблон содержит [ac]
, интерпретатор увидит опкод, который говорит "совпадает с входным символом в текущей позиции с набором, указанным в этом описании набора". В то время как скомпилированный IL содержит код, который фактически говорит: "соответствует входному символу в текущей позиции против 'a'
или 'c'
". Это специальное регистрирование и способность выполнять оптимизации на основе знаний о шаблоне являются некоторыми из основных причин, которые указывают на то, что при RegexOptions.Compiled
указании пропускной способности гораздо быстрее сопоставляется пропускная способность, чем интерпретатор.
Существует несколько недостатков RegexOptions.Compiled
. Наиболее эффективно является то, что это стоит построить. Не только все те же затраты, что и для интерпретатора, но затем необходимо скомпилировать полученное RegexNode
дерево и созданные opcodes/операнды в IL, что добавляет нетривиальные расходы. Созданный IL дополнительно необходимо скомпилировать JIT при первом использовании, что приведет к еще большему расходову при запуске. RegexOptions.Compiled
представляет собой фундаментальный компромисс между затратами на первое использование и накладные расходы на каждое последующее использование. Использование System.Reflection.Emit также препятствует использованию RegexOptions.Compiled
в определенных средах; некоторые операционные системы не позволяют динамически создавать код, а в таких системах Compiled
становится no-op.
Создание источника
В .NET 7 появился новый RegexGenerator
генератор источника. Генератор источника — это компонент, подключающийся к компилятору и расширяющий модуль компиляции с дополнительным исходным кодом. Пакет SDK для .NET (версия 7 и более поздние версии) включает генератор источника, который распознает атрибут в GeneratedRegexAttribute частичном методе, возвращаемом Regex
. Генератор источника предоставляет реализацию этого метода, содержащего всю логику для этого Regex
метода. Например, ранее вы могли написать код следующим образом:
private static readonly Regex s_abcOrDefGeneratedRegex =
new(pattern: "abc|def",
options: RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static void EvaluateText(string text)
{
if (s_abcOrDefGeneratedRegex.IsMatch(text))
{
// Take action with matching text
}
}
Чтобы использовать генератор источника, необходимо переписать предыдущий код следующим образом:
[GeneratedRegex("abc|def", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex AbcOrDefGeneratedRegex();
private static void EvaluateText(string text)
{
if (AbcOrDefGeneratedRegex().IsMatch(text))
{
// Take action with matching text
}
}
Совет
Флаг RegexOptions.Compiled
игнорируется генератором источника, поэтому он не нужен в исходной версии.
Созданная реализация AbcOrDefGeneratedRegex()
аналогично кэширует одинтонный Regex
экземпляр, поэтому для использования кода не требуется дополнительное кэширование.
На следующем рисунке показан снимок экрана исходного созданного кэшированного экземпляра в internal
Regex
подкласс, который генерирует генератор источника:
Но, как видно, это не просто делает new Regex(...)
. Скорее, генератор источника создает в виде кода C# пользовательскую Regex
производную реализацию с логикой, аналогичной тому, что RegexOptions.Compiled
выдает в IL. Вы получаете все преимущества RegexOptions.Compiled
производительности пропускной способности (больше, на самом деле) и преимущества Regex.CompileToAssembly
запуска, но без сложности CompileToAssembly
. Источник, который создается, является частью проекта, что означает, что он также легко просматривается и отлаживаться.
Совет
В Visual Studio щелкните правой кнопкой мыши объявление частичного метода и выберите "Перейти к определению". Кроме того, выберите узел проекта в Обозреватель решений, а затем разверните узел анализаторов>зависимостей>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.RegexGenerator>RegexGenerator.g.cs, чтобы просмотреть созданный код C# из этого генератора regex.
Вы можете задать точки останова в нем, вы можете выполнить его, и вы можете использовать его в качестве средства обучения, чтобы точно понять, как обработчик регулярных выражений обрабатывает шаблон с помощью входных данных. Генератор даже создает комментарии с тройной косой чертой (XML), чтобы сделать выражение понятным на первый взгляд и где он используется.
Внутри исходных файлов
При использовании .NET 7 исходный генератор и RegexCompiler
почти полностью перезаписали, принципиально изменяя структуру созданного кода. Этот подход был расширен для обработки всех конструкций (с одним предупреждением), и RegexCompiler
оба и генератор источника по-прежнему сопоставляются в основном 1:1 друг с другом, следуя новому подходу. Рассмотрим выходные данные генератора источника для одной из основных функций из abc|def
выражения:
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match with 2 alternative expressions, atomically.
{
if (slice.IsEmpty)
{
return false; // The input didn't match.
}
switch (slice[0])
{
case 'A' or 'a':
if ((uint)slice.Length < 3 ||
!slice.Slice(1).StartsWith("bc", StringComparison.OrdinalIgnoreCase)) // Match the string "bc" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 3;
slice = inputSpan.Slice(pos);
break;
case 'D' or 'd':
if ((uint)slice.Length < 3 ||
!slice.Slice(1).StartsWith("ef", StringComparison.OrdinalIgnoreCase)) // Match the string "ef" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 3;
slice = inputSpan.Slice(pos);
break;
default:
return false; // The input didn't match.
}
}
// The input matched.
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
Цель исходного кода заключается в том, чтобы быть понятным, с простой структурой, с комментариями, объясняющими, что делается на каждом шаге, и в целом с кодом, созданным в соответствии с руководящим принципом, что генератор должен выдавать код, как будто человек написал его. Даже при использовании обратного отслеживания структура обратного отслеживания становится частью структуры кода, а не на основе стека, чтобы указать, куда перейти дальше. Например, ниже приведен код для той же созданной функции сопоставления, если выражение :[ab]*[bc]
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
int charloop_starting_pos = 0, charloop_ending_pos = 0;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match a character in the set [ABab] greedily any number of times.
//{
charloop_starting_pos = pos;
int iteration = slice.IndexOfAnyExcept(Utilities.s_ascii_600000006000000);
if (iteration < 0)
{
iteration = slice.Length;
}
slice = slice.Slice(iteration);
pos += iteration;
charloop_ending_pos = pos;
goto CharLoopEnd;
CharLoopBacktrack:
if (Utilities.s_hasTimeout)
{
base.CheckTimeout();
}
if (charloop_starting_pos >= charloop_ending_pos ||
(charloop_ending_pos = inputSpan.Slice(charloop_starting_pos, charloop_ending_pos - charloop_starting_pos).LastIndexOfAny(Utilities.s_ascii_C0000000C000000)) < 0)
{
return false; // The input didn't match.
}
charloop_ending_pos += charloop_starting_pos;
pos = charloop_ending_pos;
slice = inputSpan.Slice(pos);
CharLoopEnd:
//}
// Advance the next matching position.
if (base.runtextpos < pos)
{
base.runtextpos = pos;
}
// Match a character in the set [BCbc].
if (slice.IsEmpty || ((uint)((slice[0] | 0x20) - 'b') > (uint)('c' - 'b')))
{
goto CharLoopBacktrack;
}
// The input matched.
pos++;
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
Структуру обратного отслеживания в коде можно увидеть с меткой, CharLoopBacktrack
выдаваемой для того, чтобы вернуться к ней и goto
перейти в это расположение при сбое последующей части регрессии.
Если вы посмотрите на реализацию RegexCompiler
кода и генератор источника, они будут выглядеть очень похоже: аналогичные методы, аналогичные структуры вызовов и даже аналогичные комментарии во всей реализации. В большинстве случаев они приводят к тому же коду, хотя один в IL и один в C#. Конечно, компилятор C# отвечает за преобразование C# в IL, поэтому результирующий IL в обоих случаях, скорее всего, не будет идентичным. Генератор источника зависит от того, что в различных случаях используется, используя тот факт, что компилятор C# будет дополнительно оптимизировать различные конструкции C#. Существует несколько конкретных вещей, которые генератор источника, таким образом, создаст более оптимизированный код сопоставления, чем это делает RegexCompiler
. Например, в одном из предыдущих примеров можно увидеть генератор источника, создающий инструкцию switch, с одной ветвью для 'a'
одной ветви и другой ветви.'b'
Так как компилятор C# очень хорошо оптимизирован для оптимизации инструкций коммутатора, с несколькими стратегиями в своем распоряжении для эффективного выполнения этого, генератор источника имеет специальную оптимизацию, которая RegexCompiler
не выполняется. Для изменения исходный генератор смотрит на все ветви, и если это может доказать, что каждая ветвь начинается с другого начального символа, она будет выдавать оператор switch над этим первым символом и не выводить любой код обратного отслеживания для этого изменения.
Вот немного более сложный пример этого. Изменения более тщательно анализируются, чтобы определить, можно ли рефакторингировать их таким образом, чтобы сделать их более легко оптимизированными для подсистем обратного отслеживания и что приведет к более простому исходному коду. Одна из таких оптимизаций поддерживает извлечение общих префиксов из ветвей, и если изменение атомарно, что упорядочивание не имеет значения, переупорядочение ветвей, чтобы обеспечить больше такого извлечения. Вы можете увидеть влияние этого для следующего шаблона Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday
дня недели, которое создает соответствующую функцию, как показано ниже:
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
char ch;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match with 6 alternative expressions, atomically.
{
int alternation_starting_pos = pos;
// Branch 0
{
if ((uint)slice.Length < 6 ||
!slice.StartsWith("monday", StringComparison.OrdinalIgnoreCase)) // Match the string "monday" (ordinal case-insensitive)
{
goto AlternationBranch;
}
pos += 6;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 1
{
if ((uint)slice.Length < 7 ||
!slice.StartsWith("tuesday", StringComparison.OrdinalIgnoreCase)) // Match the string "tuesday" (ordinal case-insensitive)
{
goto AlternationBranch1;
}
pos += 7;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch1:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 2
{
if ((uint)slice.Length < 9 ||
!slice.StartsWith("wednesday", StringComparison.OrdinalIgnoreCase)) // Match the string "wednesday" (ordinal case-insensitive)
{
goto AlternationBranch2;
}
pos += 9;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch2:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 3
{
if ((uint)slice.Length < 8 ||
!slice.StartsWith("thursday", StringComparison.OrdinalIgnoreCase)) // Match the string "thursday" (ordinal case-insensitive)
{
goto AlternationBranch3;
}
pos += 8;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch3:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 4
{
if ((uint)slice.Length < 6 ||
!slice.StartsWith("fr", StringComparison.OrdinalIgnoreCase) || // Match the string "fr" (ordinal case-insensitive)
((((ch = slice[2]) | 0x20) != 'i') & (ch != 'İ')) || // Match a character in the set [Ii\u0130].
!slice.Slice(3).StartsWith("day", StringComparison.OrdinalIgnoreCase)) // Match the string "day" (ordinal case-insensitive)
{
goto AlternationBranch4;
}
pos += 6;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch4:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 5
{
// Match a character in the set [Ss].
if (slice.IsEmpty || ((slice[0] | 0x20) != 's'))
{
return false; // The input didn't match.
}
// Match with 2 alternative expressions, atomically.
{
if ((uint)slice.Length < 2)
{
return false; // The input didn't match.
}
switch (slice[1])
{
case 'A' or 'a':
if ((uint)slice.Length < 8 ||
!slice.Slice(2).StartsWith("turday", StringComparison.OrdinalIgnoreCase)) // Match the string "turday" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 8;
slice = inputSpan.Slice(pos);
break;
case 'U' or 'u':
if ((uint)slice.Length < 6 ||
!slice.Slice(2).StartsWith("nday", StringComparison.OrdinalIgnoreCase)) // Match the string "nday" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 6;
slice = inputSpan.Slice(pos);
break;
default:
return false; // The input didn't match.
}
}
}
AlternationMatch:;
}
// The input matched.
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
В то же время генератор источника имеет другие проблемы, с которыми можно бороться, что просто не существует при выходе в IL напрямую. Если вы посмотрите несколько примеров кода назад, вы можете увидеть некоторые фигурные скобки несколько странно закомментированы. Это не ошибка. Генератор источника признает, что, если эти фигурные скобки не были закомментированы, структура обратного отслеживания зависит от перехода извне области на метку, определенную внутри этой области; такая метка не будет видна таким goto
образом, и код не сможет компилироваться. Таким образом, генератор источника должен избежать того, чтобы область была в пути. В некоторых случаях это просто закомментирует область, как было сделано здесь. В других случаях, когда это невозможно, иногда может избежать конструкций, требующих областей (таких как блок с несколькими операторами if
), если это будет проблематично.
Генератор источника обрабатывает все RegexCompiler
дескрипторы, за исключением одного исключения. Как и при обработке RegexOptions.IgnoreCase
, реализации теперь используют таблицу регистра для создания наборов во время строительства, а IgnoreCase
также сведения о сопоставлении обратной ссылки должны обращаться к этой таблице регистра. Эта таблица является внутренней System.Text.RegularExpressions.dll
, и сейчас, по крайней мере, код, внешний для этой сборки (включая код, создаваемый генератором источника), не имеет доступа к нему. Это делает обработку IgnoreCase
обратных ссылок проблемой в исходном генераторе, и они не поддерживаются. Это одна конструкция, которая не поддерживается источником генератора, который поддерживается RegexCompiler
. Если вы попытаетесь использовать шаблон с одним из этих (редких), генератор источника не будет выдавать пользовательскую реализацию и вместо этого будет возвращаться к кэшированию регулярного Regex
экземпляра:
Кроме того, ни RegexCompiler
генератор источника не поддерживает новый RegexOptions.NonBacktracking
. Если указать RegexOptions.Compiled | RegexOptions.NonBacktracking
, Compiled
флаг просто будет игнорироваться, и если вы указываете NonBacktracking
источнику генератора, он будет аналогичным образом возвращаться к кэшированию регулярного Regex
экземпляра.
Сценарии использования
Общее руководство заключается в том, если вы можете использовать генератор источника, используйте его. Если вы используете сегодня в C# с аргументами, известными во время компиляции, и особенно если вы уже используете Regex
RegexOptions.Compiled
(так как ретекс был определен как горячая точка, которая будет пользоваться более быстрой пропускной способностью), следует использовать генератор источника. Генератор источника дает следующие преимущества:
- Все преимущества пропускной способности
RegexOptions.Compiled
. - Преимущества запуска не требуется выполнять все синтаксический анализ, анализ и компиляцию во время выполнения.
- Возможность использования предварительной компиляции с кодом, созданным для регулярного выражения.
- Улучшена отладка и понимание регрессии.
- Возможность уменьшить размер обрезаемого приложения путем обрезки больших полос кода, связанных с
RegexCompiler
(и потенциально даже отражение выдает себя).
При использовании с параметром, например RegexOptions.NonBacktracking
, для которого генератор источника не может создать пользовательскую реализацию, он по-прежнему выдает кэширование и XML-комментарии, описывающие реализацию, что делает его ценным. Основной недостатком исходного генератора является то, что он выдает дополнительный код в сборку, поэтому существует потенциал для увеличения размера. Чем больше регексов в приложении, так и чем больше, тем больше код будет выдаваться для них. В некоторых ситуациях, как RegexOptions.Compiled
это может быть ненужным, так что также может быть источником генератора. Например, если у вас есть regex, который требуется только редко и для какой пропускной способности не имеет значения, это может быть более полезно просто полагаться на интерпретатор для этого спорадического использования.
Внимание
.NET 7 включает анализатор, определяющий использованиеRegex
, которое может быть преобразовано в исходный генератор, и средство исправления, которое выполняет преобразование для вас: