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

时间:2015-05-31 14:08:13

标签: c# performance x86 64-bit clr

我目前正在进行微基准测试,以便更好地了解clr性能和版本问题。有问题的微基准测试是将每个64字节的两个字节数组合在一起。

在尝试用unsafe打败.net框架实现之前,我总是使用安全的.net进行参考实现。

我的参考实施是:

for (int p = 0; p < 64; p++)
    a[p] ^= b[p];

其中abbyte[] a = new byte[64]并填充了来自.NET的数据。

此代码在x64上以x86上的两倍快速运行。首先我认为这没关系,因为jit会在{x = 1}}和x86上创建*long^=*long

但我的优化不安全版本:

*int^=*int

运行速度比x64参考实现快4倍。所以我对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++; } } *long^=*long编译器优化的想法是不对的。

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

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

*int^=*int

我认为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 ldloc.3

生成x86的ASM代码:

a

生成x64的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

1 个答案:

答案 0 :(得分: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
        }
    }
}

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

XorArray的实际x86程序集:

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

对于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

结论:x64优化器工作得更好。虽然它仍在使用byte大小的传输,但它将循环展开了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 

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

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