【问题标题】:Why Compiled RegEx performance is slower than Intrepreted RegEx?为什么编译的 RegEx 性能比 Intrepreted RegEx 慢?
【发布时间】:2011-08-25 16:16:25
【问题描述】:

我遇到了这篇文章:

Performance: Compiled vs. Interpreted Regular Expressions,我修改了示例代码以编译 1000 个正则表达式,然后每个运行 500 次以利用预编译,但即使在这种情况下,解释的正则表达式运行速度也快 4 倍!

这意味着RegexOptions.Compiled选项完全没用,实际上更糟糕的是,它更慢! 很大的不同是由于JIT,在解决JIT后编译的正则表达式在下面的代码中仍然执行有点慢对我来说没有意义,但@Jim in the answers provided a much cleaner version which works as expected

谁能解释为什么会这样?

代码,取自并修改自博文:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace RegExTester
{
    class Program
    {
        static void Main(string[] args)
        {
            DateTime startTime = DateTime.Now;

            for (int i = 0; i < 1000; i++)
            {
                CheckForMatches("some random text with email address, address@domain200.com" + i.ToString());    
            }


            double msTaken = DateTime.Now.Subtract(startTime).TotalMilliseconds;
            Console.WriteLine("Full Run: " + msTaken);


            startTime = DateTime.Now;

            for (int i = 0; i < 1000; i++)
            {
                CheckForMatches("some random text with email address, address@domain200.com" + i.ToString());
            }


            msTaken = DateTime.Now.Subtract(startTime).TotalMilliseconds;
            Console.WriteLine("Full Run: " + msTaken);

            Console.ReadLine();

        }


        private static List<Regex> _expressions;
        private static object _SyncRoot = new object();

        private static List<Regex> GetExpressions()
        {
            if (_expressions != null)
                return _expressions;

            lock (_SyncRoot)
            {
                if (_expressions == null)
                {
                    DateTime startTime = DateTime.Now;

                    List<Regex> tempExpressions = new List<Regex>();
                    string regExPattern =
                        @"^[a-zA-Z0-9]+[a-zA-Z0-9._%-]*@{0}$";

                    for (int i = 0; i < 2000; i++)
                    {
                        tempExpressions.Add(new Regex(
                            string.Format(regExPattern,
                            Regex.Escape("domain" + i.ToString() + "." +
                            (i % 3 == 0 ? ".com" : ".net"))),
                            RegexOptions.IgnoreCase));//  | RegexOptions.Compiled
                    }

                    _expressions = new List<Regex>(tempExpressions);
                    DateTime endTime = DateTime.Now;
                    double msTaken = endTime.Subtract(startTime).TotalMilliseconds;
                    Console.WriteLine("Init:" + msTaken);
                }
            }

            return _expressions;
        }

        static  List<Regex> expressions = GetExpressions();

        private static void CheckForMatches(string text)
        {

            DateTime startTime = DateTime.Now;


                foreach (Regex e in expressions)
                {
                    bool isMatch = e.IsMatch(text);
                }


            DateTime endTime = DateTime.Now;
            //double msTaken = endTime.Subtract(startTime).TotalMilliseconds;
            //Console.WriteLine("Run: " + msTaken);

        }
    }
}

【问题讨论】:

  • StopWatch 应该用于基准测试,而不是 DateTime
  • @Domenic 同意,我使用了 blogpost 中的代码,但不应以有用的方式影响此测试的结果
  • 最大的问题是编译的Regex第一次执行时,必须进行JIT编译。 JIT 编译比实际查找匹配花费的时间更多!如果你在初始化startTime之前运行一次CheckForMatches,你会发现时间更接近了。
  • @Gabe 好点 Gabe,我更新了代码,基本上运行了两次,我采取了第二次执行时间。我认为这将解决 JIT 问题。虽然它仍然较慢,但这次要接近得多。

标签: .net regex performance


【解决方案1】:

按预期使用时,编译的正则表达式匹配得更快。正如其他人指出的那样,我们的想法是编译一次并多次使用它们。构造和初始化时间是amortized 超过了这么多次运行。

我创建了一个更简单的测试,它将向您展示编译的正则表达式无疑比未编译的要快。

    const int NumIterations = 1000;
    const string TestString = "some random text with email address, address@domain200.com";
    const string Pattern = "^[a-zA-Z0-9]+[a-zA-Z0-9._%-]*@domain0\\.\\.com$";
    private static Regex NormalRegex = new Regex(Pattern, RegexOptions.IgnoreCase);
    private static Regex CompiledRegex = new Regex(Pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
    private static Regex DummyRegex = new Regex("^.$");

    static void Main(string[] args)
    {
        var DoTest = new Action<string, Regex, int>((s, r, count) =>
            {
                Console.Write("Testing {0} ... ", s);
                Stopwatch sw = Stopwatch.StartNew();
                for (int i = 0; i < count; ++i)
                {
                    bool isMatch = r.IsMatch(TestString + i.ToString());
                }
                sw.Stop();
                Console.WriteLine("{0:N0} ms", sw.ElapsedMilliseconds);
            });

        // Make sure that DoTest is JITed
        DoTest("Dummy", DummyRegex, 1);
        DoTest("Normal first time", NormalRegex, 1);
        DoTest("Normal Regex", NormalRegex, NumIterations);
        DoTest("Compiled first time", CompiledRegex, 1);
        DoTest("Compiled", CompiledRegex, NumIterations);

        Console.WriteLine();
        Console.Write("Done. Press Enter:");
        Console.ReadLine();
    }

NumIterations 设置为 500 会给我这个:

Testing Dummy ... 0 ms
Testing Normal first time ... 0 ms
Testing Normal Regex ... 1 ms
Testing Compiled first time ... 13 ms
Testing Compiled ... 1 ms

经过 500 万次迭代,我得到:

Testing Dummy ... 0 ms
Testing Normal first time ... 0 ms
Testing Normal Regex ... 17,232 ms
Testing Compiled first time ... 17 ms
Testing Compiled ... 15,299 ms

在这里您可以看到编译后的正则表达式比未编译的版本至少快 10%。

有趣的是,如果您从正则表达式中删除 RegexOptions.IgnoreCase,500 万次迭代的结果会更加惊人:

Testing Dummy ... 0 ms
Testing Normal first time ... 0 ms
Testing Normal Regex ... 12,869 ms
Testing Compiled first time ... 14 ms
Testing Compiled ... 8,332 ms

这里,编译的正则表达式比未编译的正则表达式快 35%。

在我看来,您引用的博文只是一个有缺陷的测试。

【讨论】:

  • 好的,谢谢,尤其是IgnoreCase效果也很有趣。
  • 没有将DoTest 设为实际命名方法的任何特殊原因?
  • @Dyppl:不。将其设为Action 更容易。
  • 另请注意,这些时间使用的语言环境有一个逗号作为小数点。对于我们这些 yanks,时间分别是 17.232 毫秒和 15.299 毫秒
  • @IDisposable:实际上,逗号是千位分隔符。 17 秒改变,15 秒改变。报告的数字是 500 万次迭代的总时间,而不是每次迭代的平均时间。
【解决方案2】:

http://www.codinghorror.com/blog/2005/03/to-compile-or-not-to-compile.html

仅当您将其实例化一次并多次重复使用它时,Compiled 才会有所帮助。如果您在 for 循环中创建已编译的正则表达式,那么它的性能显然会更差。您能向我们展示您的示例代码吗?

【讨论】:

  • 加了代码,我已经说过编译一次,运行500次。
  • 但是,您的代码在循环内实例化(并因此编译)正则表达式,因此您实际上编译了 500 次。
  • 是的,它会生成 1000 个已编译的正则表达式,然后运行相同的正则表达式 500 次?问题出在哪里?
  • 所以您正在执行 1000 次非常慢的操作并希望加快完成 500 次的快速操作?您应该编译一次正则表达式,然后运行正则表达式可能 10,000 次(至少)。
  • @Domenic 阅读代码,它是 1000 个编译的正则表达式,每个执行 500 次
【解决方案3】:

这个基准的问题是编译的正则表达式有创建一个全新的程序集并将其加载到 AppDomain 的开销。

编译正则表达式的设计场景(我相信——我没有设计它们)是让数十个正则表达式执行数百万次,而不是数千个正则表达式执行数千次。如果您不打算在领域内执行一百万次正则表达式,那么您甚至可能都无法弥补 JIT 编译它的时间。

【讨论】:

  • 我认为你是对的,不知何故我认为如果我们使用超过 100 次,性能优势会迅速发挥作用,但看起来并非如此。
【解决方案4】:

这几乎肯定表明您的基准代码编写不正确,而编译的正则表达式比解释的慢。使编译的正则表达式的性能有很多工作要做。

现在我们有了代码,可以查看一些需要更新的具体内容

  1. 此代码不考虑该方法的 JIT 成本。它应该运行一次代码以消除 JIT 成本,然后再次运行并测量
  2. 为什么要使用lock?完全没有必要
  3. 基准测试应使用StopWatch 而不是DateTime
  4. 要在已编译和未编译之间进行良好比较,您应该测试匹配 N 次的单个已编译 Regex 和单个未编译 Regex 的性能。不是每个正则表达式最多一次匹配的 N 个。

【讨论】:

  • @dr.邪恶我已经阅读了博客文章,但我仍然认为它是不正确的。有几个问题根本没有在测试中考虑(其中几个我已经提到,我将很快添加更多)。
  • 我也更新了我的代码,比那篇博文中的代码版本稍微好一点
  • 你说的所有事情都是正确的,但仍然不能改变编译正则表达式很慢的事实
  • 不,编译正则表达式很慢的“事实”是完全错误的,正如那些编写的基准比你所展示的相反的更胜任和准确的基准所证明的那样。
  • 很抱歉产生了敌意,但是当你提出这样一个绝对主义的答案时,当你显然不了解基准测试的工作原理时,很容易有点恼火。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-05-12
  • 2020-02-23
  • 1970-01-01
  • 1970-01-01
  • 2020-04-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多