如何提高.net正则表达式的性能?

本文关键字:性能 正则表达式 何提高 net | 更新日期: 2023-09-27 18:03:57

我有一个正则表达式,它解析Razor模板语言的一个(非常小的)子集。最近,我向regex添加了一些规则,这大大减慢了它的执行速度。我想知道:是否有某些已知是缓慢的正则表达式结构?是否需要对我正在使用的模式进行重组,以保持可读性并提高性能?注意:我已经确认这种性能下降发生在编译后。

模式如下:

new Regex(
              @"  (?<escape> '@'@ )"
            + @"| (?<comment> '@'* ( ([^'*]'@) | ('*[^'@]) | . )* '*'@ )"
            + @"| (?<using> '@using 's+ (?<namespace> ['w'.]+ ) ('s*;)? )"
            // captures expressions of the form "foreach (var [var] in [expression]) { <text>" 
/* ---> */      + @"| (?<foreach> '@foreach 's* '( 's* var 's+ (?<var> 'w+ ) 's+ in 's+ (?<expressionValue> ['w'.]+ ) 's* ') 's* '{ 's* <text> )"
            // captures expressions of the form "if ([expression]) { <text>" 
/* ---> */      + @"| (?<if> '@if 's* '( 's* (?<expressionValue> ['w'.]+ ) 's* ') 's* '{ 's* <text> )"  
            // captures the close of a razor text block
            + @"| (?<endBlock> </text> 's* '} )"
            // an expression of the form @([(int)] a.b.c)
            + @"| (?<parenAtExpression> '@'( 's* (?<castToInt> '(int')'s* )? (?<expressionValue> ['w'.]+ ) 's* ') )"
            + @"| (?<atExpression> '@ (?<expressionValue> ['w'.]+ ) )"
/* ---> */      + @"| (?<literal> ([^'@<]+|[^'@]) )",
            RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.ExplicitCapture | RegexOptions.Compiled);

/* --> */表示导致减速的新"规则"。

如何提高.net正则表达式的性能?

由于您没有锚定表达式,因此引擎必须在字符串的每个位置检查每个备选子模式,然后才能确定它找不到匹配。这总是很耗时的,但如何才能缩短时间呢?

一些想法:

我不喜欢第二行试图匹配注释的子模式,我认为它不会正确工作。

我可以看到你想用( ([^'*]'@) | ('*[^'@]) | . )*做什么-允许@*在评论中,只要它们不是分别以*@开头。但是由于该组的*量词和第三个选项.,子模式将愉快地匹配*@,因此使其他选项变得多余。

假设你要匹配的Razor子集不允许多行注释,我建议使用第二行

+ @"| (?<comment> @'*.*?'*@ )"

。延迟匹配任何字符(换行符除外),直到遇到第一个*@。您正在使用RegexOptions.ExplicitCapture,这意味着只捕获已命名的组,因此缺少()应该不是问题。

我也不喜欢最后一行的([^'@<]+|[^'@])子模式,它相当于([^'@<]+|<)[^'@<]+将贪婪地匹配到字符串的末尾,除非它遇到@<

我没有看到任何相邻的子模式匹配相同的文本,这是过度回溯的罪魁祸首,但是所有的's*似乎都是可疑的,因为它们的贪婪和灵活性,包括不匹配和换行符。也许你可以将一些's*更改为[ 't]*,因为你知道你不想匹配换行符,例如,可能在if后面的开始括号之前。

我注意到nhahtdh建议您使用use原子分组来防止引擎回溯到以前匹配的,这当然是值得尝试的,因为几乎可以肯定的是,当引擎无法再找到导致慢速的匹配时,会导致过度的回溯。

你想用RegexOptions.Multiline选项实现什么?您不希望使用^$,因此它将没有效果。

不需要转义@

正如其他人所提到的,您可以通过删除不必要的转义来提高可读性(例如转义@或转义字符类中'以外的字符;例如,用[^*]代替[^'*])。

这里有一些提高性能的想法:

将不同的选项排序,使最有可能的选项排在前面。

正则表达式引擎将尝试按照它们在正则表达式中出现的顺序匹配每个备选项。如果你把那些更可能的放在前面,那么在大多数情况下,引擎就不必浪费时间去匹配不太可能的替代方案。

删除不必要的回溯

不是你的"using"选项的结尾:@"| (?<using> '@using 's+ (?<namespace> ['w'.]+ ) ('s*;)? )"

如果由于某种原因,您有大量的空白,但在using行末尾没有关闭;,则regex引擎必须回溯每个空白字符,直到它最终决定它不能匹配('s*;)。在您的情况下,('s*;)?可以替换为's*;?,以防止在这些情况下回溯。

此外,您可以使用原子组(?>)以防止通过量词回溯(例如*+)。当你找不到匹配项时,这确实有助于提高性能。例如,您的"foreach"选项包含's* '( 's*。如果您找到文本"foreach var...",那么"foreach"选项将贪婪地匹配foreach之后的所有空白,然后在没有找到打开(时失败。然后,它将回溯,每次一个空白字符,并尝试在前一个位置匹配(,直到确认它不能匹配该行。使用原子组(?>'s*)'(将导致regex引擎在匹配时不会回溯到's*,从而使regex更快地失败。

使用它们时要小心,因为它们在错误的地方使用时可能会导致意外的失败(例如,'(?>,*);永远不会匹配任何东西,由于贪婪的.*匹配所有字符(包括;),以及原子分组(?>)防止regex引擎回溯一个字符来匹配结尾的;)。

在一些替代方案上"展开循环",例如"注释"替代方案(如果您计划为字符串添加替代方案也很有用)。

例如:@"| (?<comment> '@'* ( ([^'*]'@) | ('*[^'@]) | . )* '*'@ )"

可以替换为@"| (?<comment> @'* [^*]* ('*+[^@][^*]*)* '*+@ )"

新的正则表达式归结为:

  1. @'*:查找注释的开头@*
  2. [^*]*:读取所有"正常字符"(任何不是*的字符,因为这可能表示评论结束)
  3. ('*+[^@][^*]*)*:在注释中包含任何非终结符*
    • ('*+[^@]:如果我们发现*,确保* s的任何字符串不以@
    • 结尾
    • [^*]*:返回读取所有"正常字符"
    • )*:如果我们找到另一个*
    • ,则循环回到开始
  4. '*+@:最后,抓住*@注释的末尾,小心包含任何额外的*

你可以从Jeffrey Friedl的《Mastering regular expressions (3rd Edition)》中找到更多关于提高正则表达式性能的想法。