什么导致C#中Math.Max的不同性能?

时间:2015-02-08 14:17:26

标签: c# performance optimization

我在笔记本电脑,64位Windows 8.1,2.2 Ghz Intel Core i3上运行。代码是在发布模式下编译的,并且在没有附加调试器的情况下运行。

static void Main(string[] args)
    {
        calcMax(new[] { 1, 2 });
        calcMax2(new[] { 1, 2 });

        var A = GetArray(200000000);
        var stopwatch = new Stopwatch();
        stopwatch.Start(); stopwatch.Stop();

        GC.Collect();
        stopwatch.Reset();
        stopwatch.Start();
        calcMax(A);
        stopwatch.Stop();
        Console.WriteLine("caclMax - \t{0}", stopwatch.Elapsed);

        GC.Collect();
        stopwatch.Reset();
        stopwatch.Start();
        calcMax2(A);
        stopwatch.Stop();
        Console.WriteLine("caclMax2 - \t{0}", stopwatch.Elapsed);

        Console.ReadKey();
    }

    static int[] GetArray(int size)
    {
        var r = new Random(size);
        var ret = new int[size];

        for (int i = 0; i < size; i++)
        {
            ret[i] = r.Next();
        }
        return ret;
    }

    static int calcMax(int[] A)
    {
        int max = int.MinValue;
        for (int i = 0; i < A.Length; i++)
        {
            max = Math.Max(max, A[i]);
        }
        return max;
    }

    static int calcMax2(int[] A)
    {
        int max1 = int.MinValue;
        int max2 = int.MinValue;

        for (int i = 0; i < A.Length; i += 2)
        {
            max1 = Math.Max(max1, A[i]);
            max2 = Math.Max(max2, A[i + 1]);
        }
        return Math.Max(max1, max2);
    }

以下是程序性能的统计数据(以毫秒为单位的时间):

Framework 2.0

X86平台: 2269(calcMax) 2971(calcMax2) [赢家calcMax]

X64平台: 6163(calcMax) 5916(calcMax2) [赢家calcMax2]

Framework 4.5(以毫秒为单位的时间)

X86平台: 2109(calcMax) 2579(calcMax2) [赢家calcMax]

X64平台: 2040(calcMax) 2488(calcMax2) [赢家calcMax]

正如您所看到的,性能不同取决于框架和选择合并平台。我看到生成的IL代码,每种情况都是一样的。

calcMax2正在测试中,因为它应该使用处理器的“流水线”。但是只有64位平台上的框架2.0才会更快。那么,在不同的表现中展示案例的真正原因是什么?

3 个答案:

答案 0 :(得分:5)

只是值得一提的一些注意事项。我的处理器(Haswell i7)与你的处理器不相符,我当然无法接近再现异常值x64的结果。

基准测试是一项危险的练习,很容易犯下可能对执行时间产生重大影响的简单错误。只有在查看生成的机器代码时才能真正看到它们。使用工具+选项,调试,常规并取消选中“抑制JIT优化”选项。这样你可以使用Debug&gt;查看代码。 Windows&gt;反汇编并不影响优化器。

执行此操作时您会看到一些事情:

  • 你犯了一个错误,你实际上并没有使用方法返回值。抖动优化器在可能的情况下有这样的机会,它完全省略了calcMax()中的max变量赋值。但不是在calcMax2()中。这是一个经典的基准测试oops,在一个真正的程序中,你当然会使用返回值。这使得calcMax()看起来太好了。

  • .NET 4抖动对于优化Math.Max()更加智能,可以生成内联代码。 .NET 2抖动还不能这样做,它必须调用CLR辅助函数。因此,4.5测试应该更快地运行 lot ,它不会强烈暗示真正限制代码执行。它不是处理器的执行引擎,而是访问内存的成本。您的阵列太大而无法放入处理器缓存中,因此您的程序陷入困境,等待缓慢的RAM提供数据。如果处理器不能与执行指令重叠,那么它就会停止。

  • 值得注意的是,calcMax()是C#执行的数组边界检查所发生的事情。抖动知道如何从循环中完全消除它。然而,在calcMax2(),A[i + 1]螺丝上执行相同操作并不够智能。该检查不是免费的,它应该使calcMax2()相当慢。它不会再次强烈暗示记忆是真正的瓶颈。这是非常正常的btw,C#中的数组绑定检查可以有很低的开销,因为它比数组元素访问便宜得多。

至于你的基本任务,试图改善超标量执行机会,不,这不是处理器的工作方式。循环不是处理器的边界,它只是看到不同的比较和分支指令流,如果它们没有相互依赖性,所有这些指令都可以并发执行。你手工完成的是优化器已经完成的事情,一种叫做“循环展开”的优化。它选择不在这个特殊情况下这样做。 this post中提供了抖动优化器策略的概述。试图超越处理器和优化器是一个相当高的顺序,通过尝试帮助获得更糟糕的结果肯定并不罕见。

答案 1 :(得分:1)

您看到的许多差异都在公差范围内,因此应将其视为无差异。

基本上,这些数字显示的是,框架2.0对于X64来说是高度未优化的(在这里一点也不奇怪),总体而言,calcMax的性能略好于calcMax2。 (也就是没有意外,因为calcMax2包含更多指令。)

所以,我们学到的是有人提出一个理论,他们可以通过编写高级代码来获得更好的性能,这些代码以某种方式利用CPU的一些流水线,并且这个理论被证明是错误的。

由于数据的随机性,代码的运行时间由Math.max()中发生的失败分支预测决定。尝试减少随机性(更多连续值,其中第二个值总是更大),看看它是否为您提供了更好的见解。

答案 2 :(得分:0)

每次运行程序时,您的结果都会略有不同。 有时calcMax会赢,有时calcMax2会赢。这是因为在比较性能方面存在问题。 StopWhatch测量的是调用stopwatch.Start()之后经过的时间,直到调用stopwatch.Stop()。在这两者之间,可能会发生与代码无关的事情。例如,操作系统可以从您的流程中取出处理器并将其暂时提供给您计算机上运行的另一个流程,因为您的流程的时间片结束了。过了一会儿,您的进程会让处理器返回另一个时间片。 您的比较代码无法控制或预见到这种情况,因此整个实验不应被视为可靠。

为了最大限度地减少这种测量误差,您应该多次测量每个函数(例如,1000次),并计算所有测量的平均时间。这种测量方法倾向于显着提高结果的可靠性,因为它对统计误差更具弹性。