为什么本地数组比静态读取/写入更快?

时间:2015-05-13 04:55:57

标签: c# arrays performance

我正在编写一些基准测试来弄清楚为什么类似的纯算法(在类中没有C ++ lib / .net)在C ++中比在C#中运行得快得多,即使在考虑预期的特征差异时也是如此。虽然这样做我偶然发现了令我感到困惑的这两个测试,但是有没有人知道为什么一个人比另一个慢得多?第二个(在我的机器上需要51毫秒vs 88)的唯一区别是2个数组在方法中而不是在外部声明。在这两种情况下,数组都是在我们开始计时之前创建的。

    const int Runs = 100;
    const int Width = 5000;
    const int Height = 5000;
    const int Size = Width * Height;


    static int[] Input = Enumerable.Range(0, Size).ToArray();
    static int[] Output = new int[Size * 2];

    static int SimpleTest()
    {
        // Removing those 2 lines and using the static arrays instead give substantially slower performance, nearly half the speed!
        int[] Input = Enumerable.Range(0, Size).ToArray();
        int[] Output = new int[Size * 2];

        Stopwatch sw = new Stopwatch();
        sw.Start();

        for (int run = 0; run < Runs; run++)
        {
            int InputIndex = 0;
            for (int x = 0; x < Width; x++)
            {
                for (int y = 0; y < Height; y++)
                {
                    int pixel = Input[InputIndex];
                    var OutputIndex = InputIndex * 2;
                    Output[OutputIndex] = pixel;
                    Output[OutputIndex + 1] = pixel;
                    InputIndex++;
                }
            }
        }
        sw.Stop();
        return (int)(sw.ElapsedMilliseconds / Runs);
    }

3 个答案:

答案 0 :(得分:15)

当变量是本地变量时,编译器知道InputOutput永远不会改变,这会打开很多优化。

  • InputOutput变量的值可以保存在寄存器中。
  • Input.LengthOutput.Length可以计算一次并缓存。
  • 编译器可以证明Input[InputIndex]Output[OutputIndex]永远不会导致数组索引超出范围,因此可以优化边界检查。
  • 编译器可以观察到InputOutput的结果从未使用过,所以它可以优化循环到没有!

如果使用静态版本,则编译器无法执行这些优化。编译器必须在每次访问时重新加载InputOutput,并且必须在每个数组索引操作处执行边界检查,以防另一个线程被修改为InputOutput。 / p>

例如,如果另一个线程Input = new int[Size],则所有将来的计算都必须使用此备用Input。如果另一个线程Output = new int[1],则代码必须引发IndexOutOfRangeException

答案 1 :(得分:5)

使用32位JIT,我相信罪魁祸首就像Raymond Chen所提到的那样,当输入和输出是本地时,输入和输出可以保存在寄存器中,但是每次输入和输出都不需要重新加载。生成的程序集:

对于当地人:

<ul>
  <li ng-repeat="d in data | dayFilter:day">{{d}}</li>
</ul>

静力学:

007426F0  mov         eax,dword ptr [ebp-18h]  
007426F3  mov         edi,dword ptr [eax+4]  
                        int pixel = Input[InputIndex];
007426F6  mov         eax,dword ptr [ebp-18h]  
007426F9  cmp         edx,edi  
007426FB  jae         0074276E  
007426FD  mov         ecx,dword ptr [eax+edx*4+8] 

如您所见,011C2718 mov dword ptr [ebp-18h],edx 011C271B mov esi,dword ptr ds:[3BB7E90h] 011C2721 mov eax,dword ptr [esi+4] 011C2724 mov dword ptr [ebp-1Ch],eax int pixel = Input[InputIndex]; 011C2727 mov eax,dword ptr [ebp-1Ch] 011C272A cmp ecx,eax 011C272C jae 011C27A2 011C272E mov edi,dword ptr [esi+ecx*4+8] 访问数据段。 正如您所看到的,在两种情况下都会发生边界检查(mov esi,dword ptr ds:[3BB7E90h]),这样就无关紧要了,并且实际上没有将循环优化为任何内容。

64位JIT如何避免这个问题超出了我的范围。

以下是两种情况的完整反汇编:

快速版:

cmp-jae

慢版:

               for (int x = 0; x < Width; x++) {
007426EB  mov         dword ptr [ebp-14h],edx  
                    for (int y = 0; y < Height; y++) {
007426EE  xor         ebx,ebx  
007426F0  mov         eax,dword ptr [ebp-18h]  
007426F3  mov         edi,dword ptr [eax+4]  
                        int pixel = Input[InputIndex];
007426F6  mov         eax,dword ptr [ebp-18h]  
007426F9  cmp         edx,edi  
007426FB  jae         0074276E  
007426FD  mov         ecx,dword ptr [eax+edx*4+8]  
                        var OutputIndex = InputIndex * 2;
00742701  mov         esi,edx  
00742703  add         esi,esi  
                        Output[OutputIndex] = pixel;
00742705  mov         eax,dword ptr [ebp-1Ch]  
00742708  cmp         esi,dword ptr [eax+4]  
0074270B  jae         0074276E  
0074270D  mov         dword ptr [eax+esi*4+8],ecx  
                        Output[OutputIndex + 1] = pixel;
00742711  inc         esi  
00742712  mov         eax,dword ptr [ebp-1Ch]  
00742715  cmp         esi,dword ptr [eax+4]  
00742718  jae         0074276E  
0074271A  mov         dword ptr [eax+esi*4+8],ecx  
                        InputIndex++;
0074271E  inc         edx  
                    for (int y = 0; y < Height; y++) {
0074271F  inc         ebx  
                    for (int y = 0; y < Height; y++) {
00742720  cmp         ebx,1388h  
00742726  jl          007426F6  
                for (int x = 0; x < Width; x++) {
00742728  inc         dword ptr [ebp-14h]  
0074272B  cmp         dword ptr [ebp-14h],1388h  
00742732  jl          007426EE 

答案 2 :(得分:2)

我想知道这是否与performance difference between static and member functions类似?静态方法调用不是空检查,而实例函数调用是空检查。

另外,我看到的结果与你不同。静态数组在我的机器上运行时间更短。每次运行64.x ms,大约75.x ms。

这是我使用的完整程序。在OSX上运行Mono C#。

{{1}}