x64和x86之间字节数组访问的巨大性能差异

Mat*_*ias 4 c# clr performance 64-bit x86

我目前正在做微基准测试,以便更好地理解clr性能和版本问题.所讨论的微基准测试是将每个64字节的两个字节数组合并在一起.

在尝试使用unsafe依此类推.net框架实现之前,我总是使用安全的.net进行参考实现.

我的参考实现是:

for (int p = 0; p < 64; p++)
    a[p] ^= b[p];
Run Code Online (Sandbox Code Playgroud)

其中,abbyte[] a = new byte[64]和充满了来自.NET RNG数据.

此代码在x64上运行速度是x86上的两倍.首先,我认为这是正常的,因为JIT会使像*long^=*long了出来,并*int^=*int在x86.

但我优化的不安全版本:

fixed (byte* pA = a)
fixed (byte* pB = b)
{
    long* ppA = (long*)pA;
    long* ppB = (long*)pB;

    for (int p = 0; p < 8; p++)
    {
        *ppA ^= *ppB;

        ppA++;
        ppB++;
    }
}
Run Code Online (Sandbox Code Playgroud)

运行速度比x64参考实现快4倍.所以我对编译器的想法*long^=*long*int^=*int优化是不对的.

参考实现中的这种巨大性能差异来自何处?现在我发布了ASM代码:为什么C#编译器也不能以这种方式优化x86版本?

用于x86和x64参考实现的IL代码(它们是相同的):

IL_0059: ldloc.3
IL_005a: ldloc.s p
IL_005c: ldelema [mscorlib]System.Byte
IL_0061: dup
IL_0062: ldobj [mscorlib]System.Byte
IL_0067: ldloc.s b
IL_0069: ldloc.s p
IL_006b: ldelem.u1
IL_006c: xor
IL_006d: conv.u1
IL_006e: stobj [mscorlib]System.Byte
IL_0073: ldloc.s p
IL_0075: ldc.i4.1
IL_0076: add
IL_0077: stloc.s p

IL_0079: ldloc.s p
IL_007b: ldc.i4.s 64
IL_007d: blt.s IL_0059
Run Code Online (Sandbox Code Playgroud)

我想ldloc.3是的a.

生成的x86 ASM代码:

                for (int p = 0; p < 64; p++)
010900DF  xor         edx,edx
010900E1  mov         edi,dword ptr [ebx+4]
                    a[p] ^= b[p];
010900E4  cmp         edx,edi
010900E6  jae         0109010C
010900E8  lea         esi,[ebx+edx+8]
010900EC  mov         eax,dword ptr [ebp-14h]
010900EF  cmp         edx,dword ptr [eax+4]
010900F2  jae         0109010C
010900F4  movzx       eax,byte ptr [eax+edx+8]
010900F9  xor         byte ptr [esi],al
                for (int p = 0; p < 64; p++)
010900FB  inc         edx
010900FC  cmp         edx,40h
010900FF  jl          010900E4
Run Code Online (Sandbox Code Playgroud)

生成的x64 ASM代码:

                    a[p] ^= b[p];
00007FFF4A8B01C6  mov         eax,3Eh
00007FFF4A8B01CB  cmp         rax,rcx
00007FFF4A8B01CE  jae         00007FFF4A8B0245
00007FFF4A8B01D0  mov         rax,qword ptr [rbx+8]
00007FFF4A8B01D4  mov         r9d,3Eh
00007FFF4A8B01DA  cmp         r9,rax
00007FFF4A8B01DD  jae         00007FFF4A8B0245
00007FFF4A8B01DF  mov         r9d,3Fh
00007FFF4A8B01E5  cmp         r9,rcx
00007FFF4A8B01E8  jae         00007FFF4A8B0245
00007FFF4A8B01EA  mov         ecx,3Fh
00007FFF4A8B01EF  cmp         rcx,rax
00007FFF4A8B01F2  jae         00007FFF4A8B0245
00007FFF4A8B01F4  nop         word ptr [rax+rax]
00007FFF4A8B0200  movzx       ecx,byte ptr [rdi+rdx+10h]
00007FFF4A8B0205  movzx       eax,byte ptr [rbx+rdx+10h]
00007FFF4A8B020A  xor         ecx,eax
00007FFF4A8B020C  mov         byte ptr [rdi+rdx+10h],cl
00007FFF4A8B0210  movzx       ecx,byte ptr [rdi+rdx+11h]
00007FFF4A8B0215  movzx       eax,byte ptr [rbx+rdx+11h]
00007FFF4A8B021A  xor         ecx,eax
00007FFF4A8B021C  mov         byte ptr [rdi+rdx+11h],cl
00007FFF4A8B0220  add         rdx,2
                for (int p = 0; p < 64; p++)
00007FFF4A8B0224  cmp         rdx,40h
00007FFF4A8B0228  jl          00007FFF4A8B0200
Run Code Online (Sandbox Code Playgroud)

Ben*_*igt 5

您犯了一个经典错误,尝试对非优化代码进行性能分析.这是一个完整的最小可编译示例:

using System;

namespace SO30558357
{
    class Program
    {
        static void XorArray(byte[] a, byte[] b)
        {
            for (int p = 0; p< 64; p++)
                a[p] ^= b[p];
        }

        static void Main(string[] args)
        {
            byte[] a = new byte[64];
            byte[] b = new byte[64];
            Random r = new Random();

            r.NextBytes(a);
            r.NextBytes(b);

            XorArray(a, b);
            Console.ReadLine();  // when the program stops here
                                 // use Debug -> Attach to process
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我使用Visual Studio 2013 Update 3编译了这个,除了体系结构之外的C#控制台应用程序的默认"Release Build"设置,并使用CLR v4.0.30319运行它.哦,我认为我安装了Roslyn,但这不应该取代JIT,只能转换为MSIL,两种架构都是相同的.

实际的x86程序集XorArray:

006F00D8  push        ebp  
006F00D9  mov         ebp,esp  
006F00DB  push        edi  
006F00DC  push        esi  
006F00DD  push        ebx  
006F00DE  push        eax  
006F00DF  mov         dword ptr [ebp-10h],edx  
006F00E2  xor         edi,edi  
006F00E4  mov         ebx,dword ptr [ecx+4]  
006F00E7  cmp         edi,ebx  
006F00E9  jae         006F010F  
006F00EB  lea         esi,[ecx+edi+8]  
006F00EF  movzx       eax,byte ptr [esi]  
006F00F2  mov         edx,dword ptr [ebp-10h]  
006F00F5  cmp         edi,dword ptr [edx+4]  
006F00F8  jae         006F010F  
006F00FA  movzx       edx,byte ptr [edx+edi+8]  
006F00FF  xor         eax,edx  
006F0101  mov         byte ptr [esi],al  
006F0103  inc         edi  
006F0104  cmp         edi,40h  
006F0107  jl          006F00E7  
006F0109  pop         ecx  
006F010A  pop         ebx  
006F010B  pop         esi  
006F010C  pop         edi  
006F010D  pop         ebp  
006F010E  ret
Run Code Online (Sandbox Code Playgroud)

而对于x64:

00007FFD4A3000FB  mov         rax,qword ptr [rsi+8]  
00007FFD4A3000FF  mov         rax,qword ptr [rbp+8]  
00007FFD4A300103  nop         word ptr [rax+rax]  
00007FFD4A300110  movzx       ecx,byte ptr [rsi+rdx+10h]  
00007FFD4A300115  movzx       eax,byte ptr [rdx+rbp+10h]  
00007FFD4A30011A  xor         ecx,eax  
00007FFD4A30011C  mov         byte ptr [rsi+rdx+10h],cl  
00007FFD4A300120  movzx       ecx,byte ptr [rsi+rdx+11h]  
00007FFD4A300125  movzx       eax,byte ptr [rdx+rbp+11h]  
00007FFD4A30012A  xor         ecx,eax  
00007FFD4A30012C  mov         byte ptr [rsi+rdx+11h],cl  
00007FFD4A300130  movzx       ecx,byte ptr [rsi+rdx+12h]  
00007FFD4A300135  movzx       eax,byte ptr [rdx+rbp+12h]  
00007FFD4A30013A  xor         ecx,eax  
00007FFD4A30013C  mov         byte ptr [rsi+rdx+12h],cl  
00007FFD4A300140  movzx       ecx,byte ptr [rsi+rdx+13h]  
00007FFD4A300145  movzx       eax,byte ptr [rdx+rbp+13h]  
00007FFD4A30014A  xor         ecx,eax  
00007FFD4A30014C  mov         byte ptr [rsi+rdx+13h],cl  
00007FFD4A300150  add         rdx,4  
00007FFD4A300154  cmp         rdx,40h  
00007FFD4A300158  jl          00007FFD4A300110
Run Code Online (Sandbox Code Playgroud)

底线:x64优化器工作得更好.虽然它仍在使用byte-sized传输,但它将循环展开了4倍,并内联函数调用.

因为在x86版本中,循环控制逻辑大约相当于代码的一半,所以展开可以预期产生几乎两倍的性能.

内联允许编译器执行上下文相关的优化,知道数组的大小并消除运行时边界检查.

如果我们手动内联,x86编译器现在产生:

00A000B1  xor         edi,edi  
00A000B3  mov         eax,dword ptr [ebp-10h]  
00A000B6  mov         ebx,dword ptr [eax+4]  
                a[p] ^= b[p];
00A000B9  mov         eax,dword ptr [ebp-10h]  
00A000BC  cmp         edi,ebx  
00A000BE  jae         00A000F5  
00A000C0  lea         esi,[eax+edi+8]  
00A000C4  movzx       eax,byte ptr [esi]  
00A000C7  mov         edx,dword ptr [ebp-14h]  
00A000CA  cmp         edi,dword ptr [edx+4]  
00A000CD  jae         00A000F5  
00A000CF  movzx       edx,byte ptr [edx+edi+8]  
00A000D4  xor         eax,edx  
00A000D6  mov         byte ptr [esi],al  
            for (int p = 0; p< 64; p++)
00A000D8  inc         edi  
00A000D9  cmp         edi,40h  
00A000DC  jl          00A000B9 
Run Code Online (Sandbox Code Playgroud)

没有那么多帮助,循环仍然没有展开,运行时边界检查仍然存在.

值得注意的是,x86编译器找到了一个寄存器(EBX)来缓存一个数组的长度,但是用完寄存器并被强制在每次迭代时从内存中访问另一个数组长度.这应该是一个"廉价"的L1缓存访问,但这仍然比寄存器访问慢,并且比没有边界检查要慢得多.