C# x64和x86之间在字节数组访问方面的巨大性能差异
为了更好地理解clr性能和版本问题,我目前正在进行微基准测试。讨论中的微基准是将每个64字节的两个字节数组xoring在一起 我总是先用safe.net做一个参考实现,然后再尝试用C# x64和x86之间在字节数组访问方面的巨大性能差异,c#,performance,x86,64-bit,clr,C#,Performance,X86,64 Bit,Clr,为了更好地理解clr性能和版本问题,我目前正在进行微基准测试。讨论中的微基准是将每个64字节的两个字节数组xoring在一起 我总是先用safe.net做一个参考实现,然后再尝试用safe等来击败.net框架实现 我的参考实施方案是: for (int p = 0; p < 64; p++) a[p] ^= b[p]; 运行速度大约是x64参考实现的4倍。因此,我对编译器的*long^=*long和*int^=*int优化的想法是不正确的 参考实现中的巨大性能差异来自何处?现在我
safe
等来击败.net框架实现
我的参考实施方案是:
for (int p = 0; p < 64; p++)
a[p] ^= b[p];
运行速度大约是x64参考实现的4倍。因此,我对编译器的*long^=*long
和*int^=*int
优化的想法是不正确的
参考实现中的巨大性能差异来自何处?现在我发布了ASM代码:为什么C编译器不能以这种方式优化x86版本?
x86和x64参考实现的IL代码(它们相同):
我认为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
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,德沃德ptr[eax+4]
010900F2 jae 0109010C
010900F4 movzx eax,字节ptr[eax+edx+8]
010900F9异或字节ptr[esi],al
对于(int p=0;p<64;p++)
010900FB公司edx
010900FC cmp edx,40小时
010900FF jl 010900E4
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
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凸轮轴位置r9,rcx
00007FFF4A8B01E8 jae 00007FFF4A8B0245
00007FFF4A8B01EA mov ecx,3Fh
00007FFF4A8B01EF cmp rcx,rax
00007FFF4A8B01F2 jae 00007FFF4A8B0245
00007FFF4A8B01F4 nop字ptr[rax+rax]
00007FFF4A8B0200 movzx ecx,字节ptr[rdi+rdx+10h]
00007FFF4A8B0205 movzx eax,字节ptr[rbx+rdx+10h]
00007FFF4A8B020A异或ecx,eax
00007FFF4A8B020C mov字节ptr[rdi+rdx+10h],cl
00007FFF4A8B0210 movzx ecx,字节ptr[rdi+rdx+11h]
00007FFF4A8B015 movzx eax,字节ptr[rbx+rdx+11h]
00007FFF4A8B01A异或ecx,eax
00007FFF4A8B01C mov字节ptr[rdi+rdx+11h],cl
00007FFF4A8B0220添加rdx,2
对于(int p=0;p<64;p++)
00007FFF4A8B0224 cmp rdx,40h
00007FFF4A8B0228 jl 00007FFF4A8B0200
您犯了一个典型的错误,试图对未优化的代码进行性能分析。下面是一个完整的最小可编译示例:
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
}
}
}
对于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优化器工作得更好。当它仍然使用字节大小的传输时,它将循环展开了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
00A000B1异或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,字节ptr[esi]
00A000C7 mov edx,dword ptr[ebp-14h]
00A000CA cmp edi,dword ptr[edx+4]
00A000CD在00A000F5
00A000CF movzx edx,字节ptr[edx+edi+8]
00A000D4异或eax,edx
00A000D6 mov字节ptr[esi],al
对于(int p=0;p<64;p++)
00A000D8公司edi
00A000D9 cmp edi,40小时
00A000DC jl 00A000B9
没有太大帮助,循环仍然没有展开,运行时边界检查仍然存在
值得注意的是,x86编译器找到了一个寄存器(
EBX
)来缓存一个数组的长度,但寄存器用完了,每次迭代都被迫从内存访问另一个数组长度。这应该是一个“便宜”的一级缓存访问,但仍然比寄存器访问慢,而且比没有边界检查慢得多。发布两个版本的反汇编列表。一般来说,JIT并不聪明。应该是原始的、按字面意思编译的代码。此外,还应使用.Length作为上限,以至少消除一次阵列访问检查。即使在您编写的版本中,RyuJIT也能够消除这两种检查。这是我能找到的关于RyuJIT的唯一好处。我添加了从C#编译器生成的IL。它们是相同的。那么为您的代码生成的IL和编译器生成的IL呢?那两个也一样吗?我认为这就是这里应该比较的。区别可能存在于单个指令中,比如数组边界检查或类似的东西。我们需要x86代码,IL是直译的,没有意义。它是便携式的,所以它总是相同的。右键单击,显示反汇编。@Matthias进入调试设置并取消选中“抑制JIT优化”。然后您可以看到正确的拆卸。
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
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
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