【问题标题】:Why is a cached Regexp outperforming a compiled one?为什么缓存的正则表达式优于已编译的正则表达式?
【发布时间】:2009-01-09 14:18:43
【问题描述】:

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

我编写了这个简单的小基准测试。它以随机顺序调用 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

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

感谢您的回答。

【问题讨论】:

  • 我希望看到一个更新的基准测试,它忽略了第一次匹配。这样,我们可以看到正则表达式被 Regex.Match 缓存,并且与编译的时间相对相同。

标签: c# regex performance benchmarking


【解决方案1】:

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

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

【讨论】:

  • 啊。 :-) 谢谢。我会在一分钟内用新结果更新我的帖子。
【解决方案2】:

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

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


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

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

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

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

【讨论】:

  • 感谢您的解释。就我而言,第一步似乎不会产生太多成本(请参阅新结果)。在发布此之前,我阅读了 Jeff Atwood 的帖子。所以我知道缺点。在我的情况下,编译选项会有所帮助,尽管在标准用例中没有那么大。
  • Jeff Atwood 的帖子 已移动:To Compile or Not To Compile (03 Mar 2005)
【解决方案3】:

这是来自文档;

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

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

与解释的正则表达式相比,编译的正则表达式 表达式会增加启动时间,但会单独执行 模式匹配方法更快。因此,性能优势 编译正则表达式的结果在 与调用的正则表达式方法的数量成比例。


总而言之,我们建议您在调用具有特定含义的正则表达式方法时使用解释正则表达式 正则表达式相对不常见。

当你调用正则时你应该使用编译的正则表达式 具有特定正则表达式的表达式方法相对 经常。


如何检测?

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


编译的正则表达式:

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

  1. 如果您是组件开发人员,想要 创建一个可重用的正则表达式库。
  2. 如果您期望 您的正则表达式的模式匹配方法被称为 不确定的次数——从一次或两次到 数千或数万次。不同于编译或 解释的正则表达式,编译的正则表达式 分离组件提供始终如一的性能 方法调用的数量。

【讨论】:

    【解决方案4】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-09-09
      • 2011-06-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-05-08
      • 2015-12-14
      相关资源
      最近更新 更多