为什么编译的正则表达式性能比解释的正则表达式慢?

25

我看到了这篇文章:

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

这意味着RegexOptions.Compiled选项完全无用,实际上更糟糕,它更慢!大的差异是由于JIT,解决JIT后,在以下代码中编译的正则表达式仍然执行得有点慢,这对我来说没有意义,但是@Jim在回答中提供了一个更干净的版本,可以按预期工作

有人能解释一下为什么会这样吗?

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

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

        }
    }
}

6
在基准测试中应使用 StopWatch 而不是 DateTime - Domenic
@Domenic 同意,我使用了博客文章中的代码,但这不应该对此测试的结果产生有用的影响。 - dr. evil
最大的问题是第一次执行编译后的正则表达式时,需要进行即时编译。即时编译所花费的时间比实际查找匹配的时间还要长!如果在初始化startTime之前先运行一次CheckForMatches,你会发现时间差距变得更小。 - Gabe
@Gabe 不错的观点,我更新了代码,基本上运行两次并且取第二次执行时间。我假设这将解决JIT问题。虽然速度仍然较慢,但这次接近得多了。 - dr. evil
4个回答

43

当编写程序时,编译后的正则表达式能更快地匹配数据。正如其他人指出的那样,这意味着只需要编译一次,然后多次使用。构建和初始化时间在多次运行中分摊

我创建了一个简单得多的测试,可以证明编译后的正则表达式无疑比未编译的要快。

    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%。

我认为,你引用的博客文章只是一项有缺陷的测试。


1
很好,谢谢,特别是IgnoreCase效果也非常有趣。 - dr. evil
1
还要注意,这些时间使用的区域设置中小数点是逗号。对于我们美国人来说,这些时间是17.232毫秒和15.299毫秒。 - IDisposable
2
@IDisposable: 实际上,逗号是千位分隔符。这里的17秒零找,和15秒零找,是5百万次迭代的总时间,而不是平均每次迭代的时间。 - Jim Mischel
@JimMischel 我知道这是一个旧帖子,但他的意思是使用,还是.取决于语言环境。在我的国家,10.001表示一万零一。而在其他国家,它表示10和1千分之一。我认为任何人都可以想象出(简单的)单个匹配不会花费17+毫秒,考虑到您之前的数字。他只是指在我们的语言环境中,它写作17.23215.299 - Aidiakapi
@PiotrPerak:我不明白你的代码与我的有何不同,所以我不明白你为什么要发布它。也许你想发布带有时间的输出?如果你的结果与我的不同,并且你想知道原因,那么你应该发布一个问题。我不明白正则表达式缓存与这个讨论有什么关系。如果它被使用了,那么它仍然不如显式编译的表达式执行得好。至少在这个测试案例中是这样的。 - Jim Mischel
显示剩余4条评论

6

我已经添加了代码,我已经说过我只编译一次,运行500次。 - dr. evil
2
但是,你的代码在循环内实例化(因此编译)正则表达式,因此实际上编译了500次。 - Domenic
它生成了1000个编译后的正则表达式,然后运行相同的正则表达式500次?问题出在哪里? - dr. evil
1
所以你正在执行一个非常缓慢的操作1000次,并希望加快500次完成的快速操作?你应该编译正则表达式一次,然后运行正则表达式大约10,000次(至少)。 - Domenic
@Domenic 请阅读代码,其中包含1000个编译的正则表达式,并且每个都会执行500次。 - dr. evil

5
这个基准测试的问题在于编译后的正则表达式需要创建一个全新的程序集并将其加载到AppDomain中,这会增加额外的开销。
编译后的正则表达式是为执行数百万次正则表达式而设计的场景(我认为——我并没有设计它们),而不是执行数千次正则表达式。如果您不打算执行数百万次正则表达式,那么您可能甚至无法弥补JIT编译所需的时间。

我认为你是对的,不知怎么的我以为如果使用超过100次,性能优势会很快显现出来,但事实并非如此。 - dr. evil

4
这几乎可以确定你的基准代码编写有误,而不是编译后的正则表达式比解释器慢。已经投入了大量工作来使编译后的正则表达式性能良好。
现在我们可以查看需要更新的几个具体事项:
  1. 此代码没有考虑方法的JIT成本。应该运行一次代码以消除JIT成本,然后再次运行并测量。
  2. 为什么要使用lock?这完全是不必要的。
  3. 基准测试应该使用StopWatch而不是DateTime
  4. 为了获得编译和未编译之间的良好比较,应测试单个编译后的Regex和单个未编译的Regex匹配N次的性能。不是每个正则表达式最多匹配一次的N个。

@Dr. Evil,我已经阅读了这篇博客文章,但我仍然认为它是不正确的。在测试中有几个问题是没有考虑到的(其中有几个我已经提到了,还有更多我很快会补充)。 - JaredPar
我也更新了我的代码,这是那篇博客文章中稍微更好的版本。 - dr. evil
你说的一切都是正确的,但编译后的正则表达式仍然很慢。 - dr. evil
不,编译后的正则表达式慢这一“事实”完全是错误的,因为已经有比你更有能力和准确的基准测试编写者证明了相反的情况。 - Domenic
2
抱歉有点冲突,但是当你明显不了解基准测试的工作原理时,却给出一个绝对的回答,很容易让人感到有些恼火。 - Domenic
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接