为什么我的正则表达式编译后比解释执行慢得多?

20

我有一个大而复杂的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 CompiledAssembly.ComplexRegex().Matches(searchText);中调用编译后的程序集时,我应该使用new吗?但如果没有它会出现"需要对象引用"错误。
更新2
感谢答案/评论。我使用的正则表达式非常长,但基本上很简单,是由数千个单词列表组成,每个单词之间用|分隔。我真的看不出这会是一个回溯问题。主题字符串可能只有一个字母长,它仍然会导致编译延迟。对于一个RegexOptions.Compiled正则表达式,当正则表达式包含5000个单词时,执行需要超过10秒钟。相比之下,正则表达式的未编译版本可以拥有30,000多个单词,并且仍然几乎立即执行。
经过大量测试后,我认为我发现了以下内容:
  • 当您的正则表达式有许多替代方案时,请不要使用RegexOptions.Compiled——编译可能非常缓慢。
  • .Net会在可能的情况下使用惰性评估来进行正则表达式匹配,至少在某种程度上,我认为这也适用于正则表达式编译。只有在必须时,正则表达式才会被完全编译,似乎没有办法提前强制编译。
  • 如果可以强制正则表达式完全编译,Regex.CompileToAssembly将更加有用,因为它现在几乎是无意义的。

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


也许你应该尝试分享涉及到的实际表达式和一个样本输入,这样可以更好地理解你所描述的行为。 - driis
1
感谢您的帖子。我也遇到了一些由Twitter从Java移植到.NET的正则表达式问题。无论是RegexOptions.Compiled还是.CompileToAssembly,都会导致应用程序在第一次尝试匹配时挂起约10秒钟。删除Regex.Compiled后,一切都变得瞬间完成。 - LongZheng
2
MSDN新增了一篇针对.NET 4的最佳实践文章,其中涉及到编译为程序集的正则表达式 - Patrick M
5个回答

8
使用RegexOptions.Compiled时,应确保重用Regex对象。看起来你没有这样做。 RegexOptions.Compiled是一种权衡。正则表达式的初始构建会更慢,因为代码是即时编译的,但每次匹配应该更快。如果您的正则表达式在运行时发生更改,则使用RegexOptions.Compiled可能没有任何好处,尽管这可能取决于实际涉及的表达式。
更新,根据评论
如果您的实际代码看起来像您发布的代码,则不会利用CompileToAssembly,因为每次运行该代码段时都会创建新的即时编译的Regex实例。为了利用CompileToAssembly,您需要先编译Regex,然后获取生成的程序集并在项目中引用它。然后应该实例化生成的强类型Regex类型。
在您链接到的示例中,他有一个名为FindTCPIP的正则表达式,它被编译为名为FindCTPIP的类型。当需要使用它时,应创建此特定类型的新实例,例如:
TheRegularExpressions.FindTCPIP MatchTCP = new TheRegularExpressions.FindTCPIP();

能否将初始构建中涉及的延迟转移到 Regex.CompileToAssembly 时间呢?我认为 CompileToAssembly 的整个想法是将构建缓慢性从运行时转移到编译时,因此不会有任何折衷。 - Michael Low
你在问题中发布的代码是你正在使用的实际代码吗?如果是这样,CompileToAssembly将没有任何效果,因为你每次都在创建新的Regex实例。 - driis
抱歉之前表述不够清晰,我已经编辑了问题。我尝试过创建新的Regex()对象或按照示例进行实例化,但两种方法都无法解决我的问题。无论哪种方式,第一次运行正则表达式时仍然存在很大的延迟,而我正试图将其从运行时转移到编译时。 - Michael Low

2
要强制初始化,您可以对空字符串调用Match。除此之外,您还可以使用ngen创建表达式的本机映像,以进一步加快过程。但最重要的是,把30,000个string.IndexOf、string.Contains或Regex.Match语句对比给定文本速度几乎与编译巨大的表达式相同。由于这需要更少的编译和jitting等操作,因为状态机更简单。
另一件事情是,您可以考虑将文本标记化,并与您想要的单词列表相交。

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

这就是我已经在做的事情,如果不清楚请见谅。CompileToAssembly命令几乎瞬间完成(我本以为它应该需要一点时间),而在执行正则表达式时,在foreach处出现了延迟。 - Michael Low
3
你能更新一下你的问题吗?这样做 new Regex 的话,你会构建新的、未编译的正则表达式实例。你需要使用你的正则表达式程序集中的类。 - Douglas
谢谢Douglas,我已经更新了,希望现在更清晰了一些。 - Michael Low

2

在调查慢正则表达式时,很有可能的原因是它回溯太多。解决方法是重写正则表达式,使回溯次数不存在或最小化。

您能否发布正则表达式和一个导致其变慢的示例输入。

个人而言,我没有编译正则表达式的需要,尽管如果您采取了这种方法,看到一些实际性能数字是很有趣的。


谢谢,我已经更新了帖子并添加了更多细节。我认为这不是回溯问题,因为没有RegexOptions.Compiled标志时非常快速。 - Michael Low
请注意,编写低效的正则表达式非常容易(几乎太容易了)。如果正则表达式基本上是一组交替集,则很少有优化的空间(但确实有)。要小心,因为只需要一个字符就可以引起“灾难性回溯”。http://www.regular-expressions.info/catastrophic.html我仍然对您的正则表达式和慢速案例感兴趣,想要看一下。 - buckley
还可以使用(?>子表达式)语言元素来禁用回溯。有关此内容的更多信息,请参阅MSDN上的.NET Framework中正则表达式的最佳实践 - Ronald
嗨,罗纳德。没错,你可以禁用回溯。但这也会对功能产生影响,因此正则表达式可能需要重新编写。 - buckley

1
经过我的广泛测试,我可以证实 mikel 的怀疑基本上是正确的。即使使用 Regex.CompileToAssembly() 并将生成的 DLL 静态链接到应用程序中,在第一次实际匹配调用时会出现相当大的初始延迟(至少对于涉及许多 ORed 选择的模式而言)。此外,第一次匹配调用的初始延迟取决于你要匹配的文本内容。例如,与空字符串或其他任意文本进行匹配会导致较小的初始延迟,但是在新文本中首次遇到实际正匹配时仍然会出现额外的延迟。完全保证未来的匹配都能快速完成的唯一方法是在运行时使用确实匹配的文本强制进行正匹配。当然,这会产生最大的初始延迟(以换取所有未来匹配都能快速完成)。
我深入挖掘以更好地理解这个问题。针对编译到程序集中的每个正则表达式,都会写入一组三元类,其命名模板如下:{RegexNameRegexNameFactoryNRegexNameRunnerN}。在RegexName ctor时实例化对RegexNameFactoryN类的引用,但不会实例化RegexNameRunnerN类。请查看基础Regex类中的私有factoryrunnerref字段。runnerref是一个缓存的弱引用,指向RegexNameRunnerN对象。通过反射进行各种实验后,我可以确认这三个已编译的类的构造函数都很快,并且RegexNameFactoryN.CreateInstance()函数(返回初始的RegexNameRunnerN引用)也很快。最初的延迟发生在RegexRunner.Scan()或其调用树中的某个位置,并且因此很可能超出由Regex.CompileToAssembly()生成的编译MSIL的范围,因为此调用树涉及许多非抽象函数。这非常不幸,意味着C# Regex编译过程的性能优势只能在一定程度上延伸:在运行时,第一次遇到正匹配时总会有一些实质性的延迟(至少对于这个多ORed模式类而言)。
我推测这与非确定有限状态自动机(Nondeterministic Finite Automaton,NFA)引擎在处理模式时运行时执行一些内部缓存/实例化有关。 jessehouwing的ngen建议很有趣,可能会提高性能。我还没有进行测试。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接