已编译的 RegexOptions 是如何工作的?

当您将正则表达式标记为要编译的表达式时,在幕后会发生什么?这与缓存的正则表达式有什么不同?

使用这些信息,您如何确定与性能提高相比,计算成本是可以忽略不计的?

43118 次浏览

BCL 团队博客中的这个条目给出了一个很好的概述: “ 正则表达式性能”。

简而言之,正则表达式有三种类型(每种类型的执行速度都快于前一种类型) :

  1. 解读

    快速创造,慢速执行

  2. 已编译 (你似乎问到的那个)

    动态创建速度较慢,执行速度较快(适合在循环中执行)

  3. 预编译的

    在应用程序的编译时创建(没有运行时创建惩罚) ,快速执行

因此,如果您打算只执行一次正则表达式,或者在应用程序的非性能关键部分(即用户输入验证)执行正则表达式,那么可以使用选项1。

如果您打算在循环中运行正则表达式(即逐行解析文件) ,那么应该使用选项2。

如果你有很多永远不会改变的正则表达式,并且被大量使用,你可以选择选项3。

值得注意的是,正则表达式的性能。NET 2.0通过 MRU 缓存未编译的正则表达式得到了改进。Regex 库代码不再每次重新解释相同的未编译正则表达式。

因此,使用已编译的正则表达式和动态正则表达式可能会有更大的性能。除了较慢的加载时间,系统还使用更多的内存将正则表达式编译成操作码。

本质上,当前的建议是不要编译正则表达式,或者事先将它们编译成单独的程序集。

参考文献: BCL 团队博客 正则表达式性能[ DavidGutierrez ]

RegexOptions.Compiled指示正则表达式引擎使用轻量级代码生成(立法会)将正则表达式编译成 IL。这种编译发生在对象的构造过程中,而 非常严重会减慢它的速度。反过来,使用正则表达式的匹配速度更快。

如果不指定此标志,则将正则表达式视为“已解释”。

举个例子:

public static void TimeAction(string description, int times, Action func)
{
// warmup
func();


var watch = new Stopwatch();
watch.Start();
for (int i = 0; i < times; i++)
{
func();
}
watch.Stop();
Console.Write(description);
Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
}


static void Main(string[] args)
{
var simple = "^\\d+$";
var medium = @"^((to|from)\W)?(?<url>http://[\w\.:]+)/questions/(?<questionId>\d+)(/(\w|-)*)?(/(?<answerId>\d+))?";
var complex = @"^(([^<>()[\]\\.,;:\s@""]+"
+ @"(\.[^<>()[\]\\.,;:\s@""]+)*)|("".+""))@"
+ @"((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}"
+ @"\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+"
+ @"[a-zA-Z]{2,}))$";




string[] numbers = new string[] {"1","two", "8378373", "38737", "3873783z"};
string[] emails = new string[] { "sam@sam.com", "sss@s", "sjg@ddd.com.au.au", "onelongemail@oneverylongemail.com" };


foreach (var item in new[] {
new {Pattern = simple, Matches = numbers, Name = "Simple number match"},
new {Pattern = medium, Matches = emails, Name = "Simple email match"},
new {Pattern = complex, Matches = emails, Name = "Complex email match"}
})
{
int i = 0;
Regex regex;


TimeAction(item.Name + " interpreted uncached single match (x1000)", 1000, () =>
{
regex = new Regex(item.Pattern);
regex.Match(item.Matches[i++ % item.Matches.Length]);
});


i = 0;
TimeAction(item.Name + " compiled uncached single match (x1000)", 1000, () =>
{
regex = new Regex(item.Pattern, RegexOptions.Compiled);
regex.Match(item.Matches[i++ % item.Matches.Length]);
});


regex = new Regex(item.Pattern);
i = 0;
TimeAction(item.Name + " prepared interpreted match (x1000000)", 1000000, () =>
{
regex.Match(item.Matches[i++ % item.Matches.Length]);
});


regex = new Regex(item.Pattern, RegexOptions.Compiled);
i = 0;
TimeAction(item.Name + " prepared compiled match (x1000000)", 1000000, () =>
{
regex.Match(item.Matches[i++ % item.Matches.Length]);
});


}
}

它对3个不同的正则表达式执行4个测试。首先它测试一个 单身一次性匹配(已编译与未编译)。其次,它测试重用相同正则表达式的重复匹配。

在我的机器上的结果(在发行版中编译,没有附加调试器)

1000个单个匹配(构造正则表达式、匹配和处置)

Type        | Platform | Trivial Number | Simple Email Check | Ext Email Check
------------------------------------------------------------------------------
Interpreted | x86      |    4 ms        |    26 ms           |    31 ms
Interpreted | x64      |    5 ms        |    29 ms           |    35 ms
Compiled    | x86      |  913 ms        |  3775 ms           |  4487 ms
Compiled    | x64      | 3300 ms        | 21985 ms           | 22793 ms

1,000,000个匹配项-重用 Regex 对象

Type        | Platform | Trivial Number | Simple Email Check | Ext Email Check
------------------------------------------------------------------------------
Interpreted | x86      |  422 ms        |   461 ms           |  2122 ms
Interpreted | x64      |  436 ms        |   463 ms           |  2167 ms
Compiled    | x86      |  279 ms        |   166 ms           |  1268 ms
Compiled    | x64      |  281 ms        |   176 ms           |  1180 ms

这些结果表明,对于重用 Regex对象的情况,编译后的正则表达式可以更快地达到 百分之六十。在某些情况下,然而可以在 数量级以上构建较慢。

它还表明,在编译正则表达式时,.NET 的 X64版本可以是 慢5到6倍


建议在下列情况下使用 使用已编译的版本:

  1. 您不关心对象初始化成本,需要额外的性能提升。(注意我们在这里讨论的是毫秒的分数)
  2. 您稍微关心一下初始化成本,但是重复使用 Regex 对象的次数太多了,以至于在应用程序生命周期中它将对此进行补偿。

正在使用的扳手,正则表达式缓存

正则表达式引擎包含一个 LRU 缓存,其中保存了使用 Regex类上的静态方法测试的最后15个正则表达式。

例如: Regex.ReplaceRegex.Match等都使用正则表达式缓存。

通过设置 Regex.CacheSize可以增加缓存的大小。它可以在应用程序生命周期的任何时候接受大小的更改。

新的正则表达式仅在 Regex 类上缓存 静电辅助装置。但是,如果构造对象,将检查缓存(以便重用和碰撞) ,那么构造的正则表达式是 未附加到缓存中

这个缓存是一个 微不足道 LRU 缓存,它使用一个简单的双链表实现。如果您碰巧将其增加到5000,并对静态助手使用5000个不同的调用,那么每个正则表达式构造都将爬行这5000个条目,以查看它是否以前被缓存过。在检查的周围有一个 锁定,所以检查可以减少并行性并引入线程阻塞。

这个数字设置得很低,以保护自己不受这种情况的影响,尽管在某些情况下,你可能别无选择,只能增加这个数字。

我的 强烈推荐永远不会,将 RegexOptions.Compiled选项传递给静态助手。

例如:

// WARNING: bad code
Regex.IsMatch("10000", @"\\d+", RegexOptions.Compiled)

原因是,你是严重的风险错过了 LRU 缓存,这将触发 超级贵编译。此外,您不知道所依赖的库在做什么,因此几乎无法控制或预测缓存的 最好的办法大小。

参见: BCL 团队博客


注意 : 这与。NET 2.0及。NET 4.0.在4.5中有一些预期的变化可能会导致这个被修改。

这并不能回答这个问题,但我建议这样做:

[RegexGenerator($@"MyPatter")]
public partial Regex Regex_SomeRegex();

这样两全其美。它在初始化时会很快,因为它是在编译时创建的。而且使用起来也很快。