为什么我的正则表达式比编译时编译得慢得多?

时间:2010-12-27 11:48:01

标签: c# regex performance

我有一个庞大而复杂的C#正则表达式,在解释时运行正常,但有点慢。我试图通过设置RegexOptions.Compiled来加快速度,这似乎是第一次约30秒,之后立即。我试图通过首先将正则表达式编译为程序集来否定这一点,所以我的应用程序可以尽可能快。

我的问题是编译延迟发生时,是否在应用程序中编译:

Regex myComplexRegex = new Regex(regexText, RegexOptions.Compiled);
MatchCollection matches = myComplexRegex.Matches(searchText);
foreach (Match match in matches) // <--- when the one-time long delay kicks in
{

} 

或提前使用Regex.CompileToAssembly:

MatchCollection matches = new CompiledAssembly.ComplexRegex().Matches(searchText);
foreach (Match match in matches) // <--- when the one-time long delay kicks in
{

} 

这使得对程序集的编译基本没用,因为我仍然在第一次foreach调用时遇到延迟。我想要的是所有的编译延迟都是在编译时完成的(在Regex.CompileToAssembly调用中),而不是在运行时。我哪里错了?

(我用来编译成程序集的代码类似于http://www.dijksterhuis.org/regular-expressions-advanced/,如果这是相关的。)

修改

new中调用已编译的程序集时,我应该使用new CompiledAssembly.ComplexRegex().Matches(searchText);吗?它虽然没有它,但它提供了“需要对象引用”错误。

更新2

感谢您的回答/评论。我正在使用的正则表达式很长但基本上很简单,列出了数千个单词,每个单词用|分隔。我真的看不出这是一个回溯问题。主题字符串可以只有一个字母长,它仍然可以导致编译延迟。对于RegexOptions.Compiled正则表达式,当正则表达式包含5000个单词时,执行将需要10秒以上。为了比较,正则表达式的非编译版本可以使用30,000多个单词,并且仍然可以立即执行。

在对此进行了大量测试之后,我认为我发现的是:

  • 当你的正则表达式有很多选择时,不要使用RegexOptions.Compiled - 编译它可能会非常慢。
  • .Net尽可能使用lazy evaluation for regex,AFAI也可以看到这也扩展(至少在一定程度上)到正则表达式编译。正则表达式只有在必要时才会完全编译,并且似乎无法提前强制编译。
  • 如果可以强制完全编译正则表达式,那么Regex.CompileToAssembly会更有用。它似乎正在接近毫无意义。

如果我错了或错过了什么,请纠正我!

5 个答案:

答案 0 :(得分:7)

使用RegexOptions.Compiled时,应确保重新使用Regex对象。你好像不这样做。

RegexOptions.Compiled是一种权衡。正则表达式的初始构造将更慢,因为代码是即时编译的,但每个匹配应该更快。如果正则表达式在运行时更改,则使用RegexOptions.Compiled可能没有任何好处,尽管它可能取决于所涉及的实际表达式。

根据评论更新

如果您的实际代码与您发布的代码类似,那么您没有利用CompileToAssembly,因为每次运行该代码时都会创建新的,正在运行的Regex实例。为了利用CompileToAssembly,您需要先编译Regex;然后获取生成的程序集并在项目中引用它。然后,您应该实例化生成的,强类型的Regex类型。

在您链接的示例中,他有一个名为FindTCPIP的正则表达式,它被编译为名为FindCTPIP的类型。当需要使用它时,应该创建这种特定类型的新实例,例如:

TheRegularExpressions.FindTCPIP MatchTCP = new TheRegularExpressions.FindTCPIP();

答案 1 :(得分:2)

尝试使用Regex.CompileToAssembly,然后链接到程序集,以便构建Regex对象。 RegexOptions.Compiled是一个运行时选项,每次运行应用程序时仍然会重新编译正则表达式。

答案 2 :(得分:2)

调查慢速正则表达式的一个非常可能的原因是它回溯太多。这可以通过重写正则表达式来解决,以便回溯的数量不存在或最小。

你可以发布正则表达式和一个缓慢的样本输入。

就个人而言,我没有必要编译一个正则表达式,尽管如果你采用这条路径,看看有关性能的实际数字很有意思。

答案 3 :(得分:2)

要强制初始化,您可以针对空字符串调用Match。最重要的是,您可以使用ngen创建表达式的本机图像,以进一步加快进程。但可能最重要的是,对于给定的文本,抛出30.000 string.IndexOf或string.Contains或Regex.Match语句的速度基本上与编译一个巨大的大表达式以匹配单个文本一样快。因为这需要更少的编译,jitting等,因为状态机更简单。

您可以考虑的另一件事是标记文本并将其与您所追求的单词列表相交。

答案 4 :(得分:1)

经过我自己的广泛测试,我可以证实对 mikel 的怀疑在本质上是正确的。即使在使用Regex.CompileToAssembly()并将结果DLL静态链接到应用程序中时,第一个实际匹配调用也存在相当大的初始延迟(至少对于涉及许多ORed替代的模式而言)。此外,第一个匹配呼叫的初始延迟取决于您匹配的文本。例如,与空字符串或其他任意文本进行匹配将导致较少的初始延迟,但是稍后在新文本中首次遇到实际的肯定匹配时,您仍然会获得额外的延迟。完全保证将来匹配的唯一方法是闪电般的快速运行,首先是在运行时强制使用确实匹配的文本进行正匹配。当然,这可以提供最大的初始延迟(以换取所有以后的比赛都快如闪电)。

我更深入地挖掘以便更好地理解这一点。对于编译到程序集中的每个正则表达式,将使用以下命名模板编写三元组类:{RegexNameRegexNameFactoryNRegexNameRunnerN}。对RegexNameFactoryN类的引用是在RegexName ctor时实例化的,而RegexNameRunnerN类不是。请参见基类factory中的私有runnerrefRegex字段。 runnerref是对RegexNameRunnerN对象的缓存弱引用。经过各种反射实验,我可以确认所有这三个已编译类的ctor都很快,并且RegexNameFactoryN.CreateInstance()函数(返回初始RegexNameRunnerN引用)也很快。初始延迟发生在RegexRunner.Scan()内的某个地方,即它的调用树,因此很可能超出Regex.CompileToAssembly()生成的已编译MSIL的范围,因为该调用树涉及许多非抽象函数。这是非常不幸的,这意味着C#Regex编译过程的性能优势仅扩展到现在为止:在运行时,第一次遇到正匹配时总是会存在一些实质性的延迟(至少对于此类“多或”模式)。 / p>

我认为这与非确定性有限自动机(NFA)引擎在处理模式时如何在运行时执行其自身的内部缓存/实例化有关。

jessehouwing 关于ngen的建议很有趣,并且可能会提高性能。我还没有测试。