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

What does cause different performance of Math.Max in C#?

我 运行 这是笔记本电脑,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);
    }

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

框架 2.0

X86平台: 2269(计算最大值) 2971 (calcMax2) [获胜者 calcMax]

X64平台: 6163 (calcMax) 5916 (calcMax2) [获胜者 calcMax2]

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

X86平台: 2109 (calcMax) 2579 (calcMax2) [获胜者 calcMax]

X64平台: 2040(计算最大值) 2488 (calcMax2) [获胜者 calcMax]

如您所见,性能因框架和选择的编译平台而异。我看到了生成的 IL 代码,每个案例都是一样的。

calcMax2 正在测试中,因为它应该使用 "pipelining" 处理器。但只有在 64 位平台上使用 framework 2.0 速度更快。那么,展示案例表现不同的真正原因是什么?

您看到的很多差异都在容忍范围内,所以应该认为没有差异。

从本质上讲,这些数字表明 Framework 2.0 没有针对 X64 进行高度优化,(这里一点也不奇怪,)并且总体而言,calcMax 的性能略好于 calcMax2。 (这也不奇怪,因为 calcMax2 包含更多指令。)

因此,我们了解到有人提出了一种理论,即他们可以通过编写以某种方式利用 CPU 的某些流水线的高级代码来实现更好的性能,并且该理论是事实证明是错误的。

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

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

为了尽量减少这种测量误差,您应该多次测量每个功能(例如,1000 次),并计算所有测量的平均时间。这种测量方法往往会显着提高结果的可靠性,因为它对统计错误更有弹性。

只是一些值得一提的笔记。我的处理器 (Haswell i7) 与您的相比效果不佳,我当然无法接近再现离群值 x64 结果。

基准测试是一项危险的工作,很容易犯下可能对执行时间产生重大影响的简单错误。只有在查看生成的机器代码时,您才能真正看到它们。使用 Tools + Options、Debugging、General 并取消勾选 "Suppress JIT optimization" 选项。这样你就可以用 Debug > Windows > Disassembly 查看代码而不影响优化器。

执行此操作时您会看到的一些内容:

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

  • .NET 4 jitter 在优化 Math.Max() 方面更加智能,可以生成内联代码。 .NET 2 抖动还不能做到这一点,它必须调用 CLR 辅助函数。因此,4.5 测试应该 运行 快 很多 ,但它并没有强烈暗示 真正 限制了代码执行.它不是处理器的执行引擎,它是访问内存的成本。您的数组太大而无法放入处理器高速缓存,因此您的程序陷入停滞,等待缓慢的 RAM 提供数据。如果处理器无法将其与执行指令重叠,那么它就会停止。

  • calcMax() 值得注意的是 C# 执行的数组边界检查发生了什么。抖动知道如何将它从循环中完全消除。然而,在 calcMax2() 中做同样的事情还不够聪明,A[i + 1] 搞砸了。该检查不是免费的,它应该使 calcMax2() 慢很多。它没有再次强烈暗示内存是真正的瓶颈。顺便说一句,这很正常,C# 中的数组绑定检查可以有很低的开销甚至没有开销,因为它比数组元素访问便宜得多。

至于您的基本任务,即尝试提高超标量执行机会,不,这不是处理器的工作方式。循环不是处理器的边界,它只是看到不同的比较和分支指令流,如果它们没有相互依赖性,所有这些指令都可以并发执行。您手动执行的操作是优化器本身已经完成的,称为 "loop unrolling" 的优化。顺便说一句,它选择在这种特殊情况下不这样做。 this post 中提供了抖动优化器策略的概述。试图超越处理器和优化器是一项相当艰巨的任务,而通过提供帮助而得到更糟糕的结果当然并不罕见。