gpt4 book ai didi

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

转载 作者:太空狗 更新时间:2023-10-30 01:03:08 29 4
gpt4 key购买 nike

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

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

我的引用实现是:

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

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

此代码在 x64 上的运行速度是在 x86 上的两倍。首先我认为这没问题,因为 jit 会在 x86 上生成类似 *long^=*long*int^=*int 的东西。

但是我优化的不安全版本:

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++;
}
}

运行速度比 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

我认为 ldloc.3a

为 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

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

最佳答案

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

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 缓存访问,但这仍然比寄存器访问慢,而且比根本没有边界检查要慢得多。

关于c# - x64 和 x86 之间字节数组访问的巨大性能差异,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30558357/

29 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com