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

时间:2013-03-13 17:19:03

标签: c# regex performance

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

这是模式:

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);

/ * ---&gt; * /表示导致经济放缓的新“规则”。

2 个答案:

答案 0 :(得分:1)

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

一些想法:

我不喜欢第二行上试图匹配评论的子模式,我认为它不会正常工作。

我可以看到您尝试使用( ([^\*]\@) | (\*[^\@]) | . )* - 在评论中允许@*,只要它们之前没有*或分别是@。但由于该组的*量词和第三个选项.,子模式将很乐意匹配*@,因此会使其他选项变得多余。

假设您尝试匹配的Razor子集不允许多行注释,我建议第二行

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

即。懒惰地匹配任何字符(但换行符),直到遇到第一个*@。 您正在使用RegexOptions.ExplicitCapture表示只捕获了已命名的组,因此缺少()应该不是问题。

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

我没有看到任何相邻的子模式会匹配相同的文本,这是过度回溯的常见罪魁祸首,但所有\s*似乎都因为他们的贪婪和灵活性而受到怀疑,包括没有匹配任何内容和换行符。也许您可以将\s*中的部分内容更改为[ \t]*,您知道自己不想匹配换行符,例如,可能在if之后的左括号之前。

我注意到nhahtdh建议你使用原子分组来阻止引擎回溯到先前匹配的,这当然是值得尝试的东西,因为几乎可以肯定的是当引擎无法找到匹配时引起的过度回溯这导致减速。

您希望使用RegexOptions.Multiline选项实现什么目标?您不希望使用^$,因此无效。

@的转义是不必要的。

答案 1 :(得分:0)

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

以下是提高效果的一些想法:

订购不同的替代方案,以便最有可能获得优先选择。

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

删除不必要的回溯

不是“使用”替代方案的结尾:@"| (?<using> \@using \s+ (?<namespace> [\w\.]+ ) (\s*;)? )"

如果由于某种原因你有大量的空格,但在使用行的末尾没有关闭;,那么正则表达式引擎必须回溯每个空白字符,直到它最终确定它不匹配(\s*;)。在您的情况下,(\s*;)?可以替换为\s*;?,以防止在这些情况下进行回溯。

此外,您可以使用原子组(?> ... )来阻止通过量词进行回溯(例如*+)。当您找不到匹配项时,这确实有助于提高性能。例如,您的“foreach”替代方案包含\s* \( \s*。如果您找到文本"foreach var...",则“foreach”替代方案将贪婪地匹配foreach之后的所有空格,然后在找不到空缺(时失败。然后它将一次回溯一个空白字符,并尝试匹配前一个位置的(,直到它确认它与该行不匹配。使用原子组(?>\s*)\(将导致正则表达式引擎在匹配时不会通过\ s *回溯,从而允许正则表达式更快地失败。

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

“在某些替代方案上展开循环”,例如您的“评论”替代方案(如果您计划为字符串添加替代方案,也很有用)。

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

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

新的正则表达式归结为:

  1. @\*:找到评论的开头@*
  2. [^*]*:阅读所有“普通字符”(任何不是*的字符,因为这可能意味着评论的结束)
  3. (\*+[^@][^*]*)*:在评论中包含任何非终端*
    • (\*+[^@]:如果我们找到*,请确保*的任何字符串不以@
    • 结尾
    • [^*]*:回去阅读所有“普通人物”
    • )*:如果找到另一个*
    • ,则循环回到开头
  4. \*+@:最后,抓住评论*@的结尾,小心包含任何额外的*
  5. 你可以从Jeffrey Friedl的Mastering Regular Expressions (3rd Edition)找到更多改善正则表达式表现的想法。