Why the performance difference between C# (quite a bit slower) and Win32/C?
我们希望将性能关键应用程序迁移到.NET,并发现C版本比Win32/C慢30%到100%,这取决于处理器(差异更多地标记在移动T7200处理器上)。我有一个非常简单的代码示例来演示这一点。为了简洁起见,我将展示C版——C是直接翻译:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #include"stdafx.h" #include"Windows.h" int array1[100000]; int array2[100000]; int Test(); int main(int argc, char* argv[]) { int res = Test(); return 0; } int Test() { int calc,i,k; calc = 0; for (i = 0; i < 50000; i++) array1[i] = i + 2; for (i = 0; i < 50000; i++) array2[i] = 2 * i - 2; for (i = 0; i < 50000; i++) { for (k = 0; k < 50000; k++) { if (array1[i] == array2[k]) calc = calc - array2[i] + array1[k]; else calc = calc + array1[i] - array2[k]; } } return calc; } |
如果我们在win32中查看"else"的反汇编,我们有:
1 2 3 4 5 6 7 8 | 35: else calc = calc + array1[i] - array2[k]; 004011A0 jmp Test+0FCh (004011bc) 004011A2 mov eax,dword ptr [ebp-8] 004011A5 mov ecx,dword ptr [ebp-4] 004011A8 add ecx,dword ptr [eax*4+48DA70h] 004011AF mov edx,dword ptr [ebp-0Ch] 004011B2 sub ecx,dword ptr [edx*4+42BFF0h] 004011B9 mov dword ptr [ebp-4],ecx |
(这是调试中的,但请放心)
使用优化的exe上的clr调试器对优化的c版本进行反汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | else calc = calc + pev_tmp[i] - gat_tmp[k]; 000000a7 mov eax,dword ptr [ebp-4] 000000aa mov edx,dword ptr [ebp-8] 000000ad mov ecx,dword ptr [ebp-10h] 000000b0 mov ecx,dword ptr [ecx] 000000b2 cmp edx,dword ptr [ecx+4] 000000b5 jb 000000BC 000000b7 call 792BC16C 000000bc add eax,dword ptr [ecx+edx*4+8] 000000c0 mov edx,dword ptr [ebp-0Ch] 000000c3 mov ecx,dword ptr [ebp-14h] 000000c6 mov ecx,dword ptr [ecx] 000000c8 cmp edx,dword ptr [ecx+4] 000000cb jb 000000D2 000000cd call 792BC16C 000000d2 sub eax,dword ptr [ecx+edx*4+8] 000000d6 mov dword ptr [ebp-4],eax |
更多的指令,大概是性能差异的原因。
所以有三个问题:
我是在看两个程序的正确反汇编,还是工具误导了我?
如果生成的指令数量的差异不是导致差异的原因,那是什么?
除了将所有性能关键的代码保存在本机dll中之外,我们还可以做些什么呢?
提前谢谢史蒂夫
PS我最近收到了一个邀请,参加一个题为"构建性能关键型本机应用程序"的MS/Intel联合研讨会。
我相信这段代码中的主要问题是对数组进行边界检查。
如果您切换到使用C中的不安全代码,并使用指针数学,您应该能够获得相同(或可能更快)的代码。
在这个问题中,以前曾详细讨论过这个问题。
我相信您看到的是数组边界检查的结果。您可以使用不安全的代码来避免边界检查。
我相信Jiter可以识别类似于数组长度的循环的模式,并避免边界检查,但看起来您的代码不能利用这一点。
正如其他人所说,其中一个方面是边界检查。在数组访问方面,代码中还存在一些冗余。通过将内部块更改为:
1 2 3 4 5 6 7 8 9 10 | int tmp1 = array1[i]; int tmp2 = array2[k]; if (tmp1 == tmp2) { calc = calc - array2[i] + array1[k]; } else { calc = calc + tmp1 - tmp2; } |
这一变化使总时间从~8.8秒降至~5秒。
为了好玩,我尝试在Visual Studio 2010的C中构建它,并查看了Jitted反汇编:
1 2 3 4 5 6 | else calc = calc + array1[i] - array2[k]; 000000cf mov eax,dword ptr [ebp-10h] 000000d2 add eax,dword ptr [ebp-14h] 000000d5 sub eax,edx 000000d7 mov dword ptr [ebp-10h],eax |
他们对clr的4.0中的抖动做了一些改进。
C正在进行边界检查
在C不安全代码中运行计算部分时,它的性能和本机实现一样好吗?
如果您的应用程序的性能关键路径完全由未检查的数组处理组成,我建议您不要用C_重写它。
但是,如果您的应用程序在语言X中已经运行良好,我建议您不要用语言Y重写它。
你想从重写中获得什么?至少,要认真考虑混合语言的解决方案,使用已经调试过的C代码作为高性能部分,并使用C获得良好的用户界面或方便地与最新的富.NET库集成。
关于可能相关主题的较长回答。
我确信C的优化与C不同。另外,您必须期望至少有一点性能降低。.NET为带有框架的应用程序添加了另一层。
取舍是更迅速的发展,庞大的图书馆和功能,为(什么应该是)少量的速度。