10

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

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

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

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

  2. RegexOptions.Compiled

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

这是代码:

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());
}

每次我调用它时,结果都是这样的:

    未编译且未自动缓存:
    总毫秒数:6185,2704
    调整后的毫秒数:6185,2704

    已编译且未自动缓存:
    总毫秒数:2562,2519
    调整后的毫秒数:2551,56949184038

    未编译并自动缓存:
    总毫秒数:2378,823
    调整后的毫秒数:2336,3187176891

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

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

顺便说一句,这是在 .Net 3.5 和 Mono 2.2 上,它们的行为完全相同。在 Windows 上。

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

编辑1:

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

    未编译且未自动缓存:
    总毫秒数:6456,5711
    调整后的毫秒数:6456,5711

    已编译且未自动缓存:
    总毫秒数:2668,9028
    调整后的毫秒数:2657,77574842168

    未编译并自动缓存:
    总毫秒数:6637,5472
    调整后的毫秒数:6518,94897724836

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

感谢您的回答。

4

4 回答 4

4

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

var m3 = Regex.Match(pattern, item); // Wrong
var m3 = Regex.Match(item, pattern); // Correct
于 2009-01-09T14:38:19.567 回答
3

我注意到类似的行为。我也想知道为什么编译版本会慢,但注意到超过一定数量的调用,编译版本会更快。所以我深入研究了反射器,我注意到对于编译的正则表达式,仍然有一些在第一次调用时执行的设置(特别是创建适当RegexRunner对象的实例)。

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


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

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

Martin Brown注意到您将静态Regex调用的参数颠倒了(很好,Martin)。我想你会发现,如果你解决了这个问题,实例(未编译的)正则表达式每次都会击败静态调用。您还应该发现,根据我上面的发现,已编译的实例也将击败未编译的实例。

但是:在你盲目地将该选项应用于你创建的每个正则表达式之前,你真的应该阅读Jeff Atwood 关于已编译正则表达式的帖子。

于 2009-01-09T14:37:16.043 回答
1

这是来自文档;

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

当调用静态正则表达式方法,在缓存中找不到正则表达式时,正则表达式引擎将正则表达式转换为一组操作码,并存储在缓存中。然后它将这些操作代码转换为 MSIL,以便 JIT 编译器可以执行它们。解释的正则表达式以较慢的执行时间为代价减少了启动时间。正因为如此,它们最适合在少量方法调用中使用正则表达式时使用,或者如果调用正则表达式方法的确切次数未知但预计会很小。随着方法调用次数的增加,减少启动时间所带来的性能提升会被较慢的执行速度所抵消。

与解释的正则表达式相比,编译的正则表达式增加了启动时间,但执行单个模式匹配方法的速度更快。因此,编译正则表达式所带来的性能优势与调用的正则表达式方法的数量成正比。


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

当您相对频繁地调用具有特定正则表达式的正则表达式方法时,您应该使用已编译的正则表达式。


如何检测?

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


编译的正则表达式:

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

  1. If you are a component developer who wants to create a library of reusable regular expressions.
  2. If you expect your regular expression's pattern-matching methods to be called an indeterminate number of times -- anywhere from once or twice to thousands or tens of thousands of times. Unlike compiled or interpreted regular expressions, regular expressions that are compiled to separate assemblies offer performance that is consistent regardless of the number of method calls.
于 2016-03-08T22:12:54.250 回答
0

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

于 2009-01-09T14:37:55.777 回答