小编典典

RegexOptions.Compiled 是如何工作的?

all

当您将正则表达式标记为要编译的正则表达式时,幕后发生了什么?这个比较/与缓存的正则表达式有何不同?

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


阅读 104

收藏
2022-08-05

共1个答案

小编典典

RegexOptions.Compiled指示正则表达式引擎使用轻量级代码生成 (
LCG
)将正则表达式表达式编译为 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 single matches (construct Regex, Match and dispose)

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 matches - reusing the Regex object

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

这些结果表明,在重用对象的情况下,编译的正则表达式的速度可以提高 60% 。* 然而 ,在某些情况下,构建速度可能会慢 3
个数量级以上。
Regex
***

它还表明,在编译正则表达式时,.NET的 x64 版本可能会 慢 5 到 6 倍。


建议在以下情况下 使用编译版本

  1. 您不关心对象初始化成本并且需要额外的性能提升。(请注意,我们在这里谈论的是几分之一毫秒)
  2. 您有点关心初始化成本,但是重用 Regex 对象的次数如此之多,以至于它将在您的应用程序生命周期中对其进行补偿。

工作中的 Spanner,Regex 缓存

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

例如:Regex.ReplaceRegex.Match。都使用Regex缓存。

缓存的大小可以通过设置来增加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 中有一些预期的变化,可能会导致对其进行修订。

2022-08-05