14

考虑以下代码:

using System;
using System.Diagnostics;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch sw = new Stopwatch();

            for (int trial = 0; trial < 4; ++trial)
            {
                sw.Restart();
                loop1();
                Console.WriteLine("loop1() took " + sw.Elapsed);

                sw.Restart();
                loop2();
                Console.WriteLine("loop2() took " + sw.Elapsed);

                sw.Restart();
                loop3();
                Console.WriteLine("loop3() took " + sw.Elapsed);

                // Console.WriteLine(); // <-- Uncomment this and the timings change a LOT!
            }
        }

        static void loop1()
        {
            bool done = false;

            for (int i = 0; i < 100000 && !done; ++i)
            {
                for (int j = 0; j < 100000 && !done; ++j)
                {
                    for (int k = 0; k < 2; ++k)
                    {
                        if (i == 9900)
                        {
                            done = true;
                            break;
                        }
                    }
                }
            }
        }

        static void loop2()
        {
            for (int i = 0; i < 100000; ++i)
            {
                for (int j = 0; j < 100000; ++j)
                {
                    for (int k = 0; k < 2; ++k)
                    {
                        if (i == 9900)
                        {
                            goto exit;
                        }
                    }
                }
            }

        exit: return;
        }

        static void loop3()
        {
            for (int i = 0; i < 100000; ++i)
            {
                for (int j = 0; j < 100000; ++j)
                {
                    for (int k = 0; k < 2; ++k)
                    {
                        if (i == 9900)
                        {
                            k = 2;
                            j = 100000;
                            i = 100000;
                        }
                    }
                }
            }
        }
    }
}

当我使用 Visual Studio 2010 在 Windows 7 x64 上编译并运行此版本的 RELEASE x86 版本时,我得到以下时序(在 Intel Core i7 上运行)

loop1() took 00:00:01.7935267
loop2() took 00:00:01.4747297
loop3() took 00:00:05.6677592
loop1() took 00:00:01.7654008
loop2() took 00:00:01.4818888
loop3() took 00:00:05.7656440
loop1() took 00:00:01.7990239
loop2() took 00:00:01.5019258
loop3() took 00:00:05.7979425
loop1() took 00:00:01.8356245
loop2() took 00:00:01.5688070
loop3() took 00:00:05.7238753

这本身就很奇怪 - 为什么 loop3() 会比其他循环慢得多?无论如何,然后我取消注释指示的行(Console.WriteLine()),我的时间变成:

loop1() took 00:00:01.8229538
loop2() took 00:00:07.8174210
loop3() took 00:00:01.4879274

loop1() took 00:00:01.7691919
loop2() took 00:00:07.4781999
loop3() took 00:00:01.4810248

loop1() took 00:00:01.7749845
loop2() took 00:00:07.5304738
loop3() took 00:00:01.4634904

loop1() took 00:00:01.7521282
loop2() took 00:00:07.6325186
loop3() took 00:00:01.4663219

现在 loop2() 慢得多,而 loop3() 快得多。我觉得这个最好奇...

所以我有两个问题:

  1. 其他人可以复制这个吗,和
  2. 如果是这样,任何人都可以解释一下吗?

[编辑] 我应该补充一点,我可以用秒表验证这些时间,并且我正在从命令行运行测试程序(因此我们可以排除 Visual Studio 干扰它)。

附录:

我对程序进行了如下修改,以排除 JITTER 正在优化循环的可能性:

using System;
using System.Diagnostics;
using System.Text;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(test());
        }

        static string test()
        {
            Stopwatch sw = new Stopwatch();
            int total = 0;
            StringBuilder builder = new StringBuilder();

            for (int trial = 0; trial < 2; ++trial)
            {
                sw.Restart();
                total += loop1();
                builder.AppendLine("loop1() took " + sw.Elapsed);

                sw.Restart();
                total += loop2();
                builder.AppendLine("loop2() took " + sw.Elapsed);

                sw.Restart();
                total += loop3();
                builder.AppendLine("loop3() took " + sw.Elapsed);
                //builder.AppendLine(); // Uncommenting this line makes a big difference!
            }

            builder.AppendLine(total.ToString());

            return builder.ToString();
        }

        static int loop1()
        {
            bool done = false;
            int total = 0;

            for (int i = 0; i < 100000 && !done; ++i)
            {
                for (int j = 0; j < 100000 && !done; ++j)
                {
                    for (int k = 0; k < 2; ++k)
                    {
                        if (i == 9900)
                        {
                            done = true;
                            break;
                        }

                        ++total;
                    }
                }
            }

            return total;
        }

        static int loop2()
        {
            int total = 0;

            for (int i = 0; i < 100000; ++i)
            {
                for (int j = 0; j < 100000; ++j)
                {
                    for (int k = 0; k < 2; ++k)
                    {
                        if (i == 9900)
                        {
                            goto exit;
                        }

                        ++total;
                    }
                }
            }

        exit: return total;
        }

        static int loop3()
        {
            int total = 0;

            for (int i = 0; i < 100000; ++i)
            {
                for (int j = 0; j < 100000; ++j)
                {
                    for (int k = 0; k < 2; ++k)
                    {
                        if (i == 9900)
                        {
                            k = 2;
                            j = 100000;
                            i = 100000;
                        }
                        else
                        {
                            ++total;
                        }
                    }
                }
            }

            return total;
        }
    }
}

现在我的结果如下:

builder.AppendLine() 注释掉:

loop1() took 00:00:06.6509471
loop2() took 00:00:06.7322771
loop3() took 00:00:01.5361389
loop1() took 00:00:06.5746730
loop2() took 00:00:06.7051531
loop3() took 00:00:01.5027345
-1004901888

builder.AppendLine() 未注释掉:

loop1() took 00:00:06.9444200
loop2() took 00:00:02.8960563
loop3() took 00:00:01.4759535

loop1() took 00:00:06.9036553
loop2() took 00:00:03.1514154
loop3() took 00:00:01.4764172

-1004901888

当我这样做时,请注意 loop2() 时间的差异。不计算!

4

1 回答 1

5

我可以准确地复制它。另外,我可以像这样使方差消失:

    private static void Main(string[] args)
    {
        Stopwatch sw = new Stopwatch();

        for (int trial = 0; trial < 4; ++trial)
        {
            sw.Restart();
            Interlocked.MemoryBarrier();
            loop1();
            Interlocked.MemoryBarrier();
            Console.WriteLine("loop1() took " + sw.Elapsed);

            sw.Restart();
            Interlocked.MemoryBarrier();
            loop2();
            Interlocked.MemoryBarrier();
            Console.WriteLine("loop2() took " + sw.Elapsed);

            sw.Restart();
            Interlocked.MemoryBarrier();
            loop3();
            Interlocked.MemoryBarrier();
            Console.WriteLine("loop3() took " + sw.Elapsed);

            // Console.WriteLine(); // <-- Uncomment this and the timings don't change now.
        }
    }

当我使用 MemoryBarriers 运行时,无论以哪种方式运行测试,我都会得到第二种模式:

loop1() took ~1 sec
loop2() took ~7 secs
loop3() took ~1 sec

MemoryBarrier 定义:

同步内存访问如下:执行当前线程的处理器不能以这样的方式重新排序指令,即调用 MemoryBarrier 之前的内存访问在调用 MemoryBarrier 之后的内存访问之后执行。

Since the IL for the loops is identical between the 2 versions, and MemoryBarrier makes the variance disappear, I think we can conclude the variance is definitely the result of an optimization on the first pattern... maybe by the JITer... maybe by the CPU... not sure there.

My environment is the same, except I am using VS2012 & .NET 4.5 RTM.

于 2012-08-16T11:21:04.857 回答