为什么缓存的Regexp优于已编译的Regexp?

时间:2009-01-09 14:18:44

标签: c# regex performance benchmarking

这只是一个满足我好奇心的问题。 但对我来说这很有趣。

我写了这个简单的小基准。它以随机顺序调用3个Regexp执行变体几千次:

基本上,我使用相同的模式,但方式不同。

  1. 没有任何RegexOptions的普通方式。从.NET 2.0开始,这些不会被缓存。但应该“缓存”,因为它保存在一个非常全局的范围内而不是重置。

  2. 使用RegexOptions.Compiled

  3. 调用静态Regex.Match(pattern, input)并在.NET 2.0中缓存

  4. 以下是代码:

    static List<string> Strings = new List<string>();        
    static string pattern = ".*_([0-9]+)\\.([^\\.])$";
    
    static Regex Rex = new Regex(pattern);
    static Regex RexCompiled = new Regex(pattern, RegexOptions.Compiled);
    
    static Random Rand = new Random(123);
    
    static Stopwatch S1 = new Stopwatch();
    static Stopwatch S2 = new Stopwatch();
    static Stopwatch S3 = new Stopwatch();
    
    static void Main()
    {
      int k = 0;
      int c = 0;
      int c1 = 0;
      int c2 = 0;
      int c3 = 0;
    
      for (int i = 0; i < 50; i++)
      {
        Strings.Add("file_"  + Rand.Next().ToString() + ".ext");
      }
      int m = 10000;
      for (int j = 0; j < m; j++)
      {
        c = Rand.Next(1, 4);
    
        if (c == 1)
        {
          c1++;
          k = 0;
          S1.Start();
          foreach (var item in Strings)
          {
            var m1 = Rex.Match(item);
            if (m1.Success) { k++; };
          }
          S1.Stop();
        }
        else if (c == 2)
        {
          c2++;
          k = 0;
          S2.Start();
          foreach (var item in Strings)
          {
            var m2 = RexCompiled.Match(item);
            if (m2.Success) { k++; };
          }
          S2.Stop();
        }
        else if (c == 3)
        {
          c3++;
          k = 0;
          S3.Start();
          foreach (var item in Strings)
          {
            var m3 = Regex.Match(item, pattern);
            if (m3.Success) { k++; };
          }
          S3.Stop();
        }
      }
    
      Console.WriteLine("c: {0}", c1);
      Console.WriteLine("Total milliseconds: " + (S1.Elapsed.TotalMilliseconds).ToString());
      Console.WriteLine("Adjusted milliseconds: " + (S1.Elapsed.TotalMilliseconds).ToString());
    
      Console.WriteLine("c: {0}", c2);
      Console.WriteLine("Total milliseconds: " + (S2.Elapsed.TotalMilliseconds).ToString());
      Console.WriteLine("Adjusted milliseconds: " + (S2.Elapsed.TotalMilliseconds*((float)c2/(float)c1)).ToString());
    
      Console.WriteLine("c: {0}", c3);
      Console.WriteLine("Total milliseconds: " + (S3.Elapsed.TotalMilliseconds).ToString());
      Console.WriteLine("Adjusted milliseconds: " + (S3.Elapsed.TotalMilliseconds*((float)c3/(float)c1)).ToString());
    }
    

    每次我称之为结果都是:

        Not compiled and not automatically cached:
        Total milliseconds: 6185,2704
        Adjusted milliseconds: 6185,2704
    
        Compiled and not automatically cached:
        Total milliseconds: 2562,2519
        Adjusted milliseconds: 2551,56949184038
    
        Not compiled and automatically cached:
        Total milliseconds: 2378,823
        Adjusted milliseconds: 2336,3187176891
    

    所以你有它。不多,但差异大约为7-8%。

    这不是唯一的谜。我无法解释为什么第一种方式会慢得多,因为它永远不会被重新评估,而是保存在一个全局静态变量中。

    顺便说一句,这是在.Net 3.5和Mono 2.2上,表现完全一样。在Windows上。

    那么,任何想法,为什么编译的变体甚至会落后?

    EDIT1:

    修复代码后,结果现在如下所示:

        Not compiled and not automatically cached:
        Total milliseconds: 6456,5711
        Adjusted milliseconds: 6456,5711
    
        Compiled and not automatically cached:
        Total milliseconds: 2668,9028
        Adjusted milliseconds: 2657,77574842168
    
        Not compiled and automatically cached:
        Total milliseconds: 6637,5472
        Adjusted milliseconds: 6518,94897724836
    

    这几乎也淘汰了所有其他问题。

    感谢您的回答。

4 个答案:

答案 0 :(得分:4)

在Regex.Match版本中,您正在寻找模式中的输入。尝试交换参数。

var m3 = Regex.Match(pattern, item); // Wrong
var m3 = Regex.Match(item, pattern); // Correct

答案 1 :(得分:3)

I noticed类似的行为。我也想知道为什么编译版本会更慢,但是注意到在一定数量的调用之上,编译版本更快。所以我稍微挖了Reflector,我注意到对于编译的正则表达式,在第一次调用时仍然会进行一些设置(具体来说,创建适当的RegexRunner对象的实例)。 / p>

在我的测试中,我发现如果我移动构造函数并在计时器启动之外对正则表达式进行初始抛弃调用,无论我运行了多少次迭代,编译的正则表达式都会获胜。


顺便提一下,使用静态Regex方法时框架正在执行的缓存是一种仅在使用静态Regex方法时才需要的优化。这是因为对静态Regex方法的每次调用都会创建一个新的Regex对象。在Regex类的构造函数中,它必须解析模式。缓存允许后续调用静态Regex方法来重用从第一次调用解析的RegexTree,从而避免解析步骤。

在单个Regex对象上使用实例方法时,这不是问题。解析仍然只执行一次(当您创建对象时)。此外,您可以避免在构造函数中运行所有其他代码,以及堆分配(以及后续的垃圾回收)。

马丁·布朗noticed你反驳了静态Regex召唤的争论(好抓,马丁)。我想你会发现,如果你修复它,实例(未编译)正则表达式每次都会击败静态调用。您还应该发现,根据我上面的发现,编译后的实例也将击败未编译的实例。

但是:在您将所有正则表达式盲目应用于所创建的正则表达式之前,您应该真正阅读已编译的正则表达式Jeff Atwood's post

答案 2 :(得分:0)

如果您使用相同的模式不断匹配相同的字符串,这可以解释为什么缓存版本比编译版本略快。

答案 3 :(得分:0)

这是来自文档;

https://msdn.microsoft.com/en-us/library/gg578045(v=vs.110).aspx

  

当调用静态正则表达式方法并且常规时   表达式无法在缓存中找到,正则表达式引擎   将正则表达式转换为一组操作代码和存储   它们在缓存中。然后它将这些操作代码转换为MSIL   JIT编译器可以执行它们。 定期解释   表达式以较慢的执行时间为代价减少启动时间。   因此,当正则表达式为时,它们最佳使用   用于少量方法调用,或者如果确切的数量为   调用正则表达式方法是未知的,但预计会是   小。随着方法调用次数的增加,性能提升   从减少的启动时间开始,执行速度越慢   速度。

     

与解释的正则表达式相比,编译为常规   表达式增加启动时间但执行个人   模式匹配方法更快。因此,性能受益   编译正则表达式的结果增加了   与正则表达式方法的数量成比例。

  

总而言之,我们建议您在使用特定方法调用正则表达式方法时使用已解释的正则表达式   正则表达式相对较少。

     

调用常规时,应使用编译的正则表达式   表达方法与特定的正则表达式相对   频繁。

如何检测?

  

执行速度较慢的确切阈值   解释的正则表达式超过其减少的收益   启动时间,或启动时间较慢的阈值   编译的正则表达式超过了他们更快的收益   执行速度,很难确定。这取决于多种多样   因素,包括正则表达式的复杂性和   它处理的具体数据。 确定是否解释或   编译的正则表达式为您提供最佳性能   特定的应用场景,您可以使用Stopwatch类   比较他们的执行时间

编译的正则表达式:

  

我们建议您将正则表达式编译到程序集中   以下情况:

     
      
  1. 如果您是想要的组件开发人员   创建可重用正则表达式库。
  2.   
  3. 如果你期待   你的正则表达式的模式匹配方法被称为   不确定的次数 - 从一次或两次到   成千上万次。不同于编译或   解释正则表达式,编译的正则表达式   分离程序集提供的性能无论如何都是一致的   方法调用次数。
  4.