为什么C#在性能上比Win32/C慢得多?

16
我们希望将一个性能关键的应用程序迁移到 .Net,但是发现 c# 版本在处理器(移动 T7200 处理器上的差异更为明显)方面比 Win32/C 版本慢 30% 到 100% 不等。我有一个非常简单的代码示例可以证明这一点。为了简洁起见,我将只展示 C 版本,而 c# 版本是直接翻译过来的。
#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”的反汇编代码,我们有:

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

(这是在调试模式下,但请忍耐)

使用CLR调试器在优化的exe上查看优化的C#版本的反汇编代码:

                    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 

更多指令,很可能是性能差异的原因。

所以有三个问题:

  1. 我是否在查看两个程序的正确反汇编代码,还是工具误导了我?

  2. 如果生成的指令数量的差异不是原因,那是什么原因造成的差异?

  3. 除了将所有性能关键代码保留在本地DLL中,我们还能做些什么呢?

提前感谢 Steve

PS 我最近收到了一份由微软/英特尔联合举办的研讨会的邀请,标题大概是“构建性能关键的本地应用程序”...


请删除汇编指令之间的所有换行符。 - Wadih M.
一如既往地,使用性能分析工具来确定哪些操作影响了性能。 (我们无法看到您代码中花费时间的部分,因此询问我们没有意义。请使用性能分析工具进行检查)除此之外,一个简单的技巧可能是通过NGen运行C#代码。这应该会大幅提升性能。 - jalf
你正在比较哪个版本的CLR。据我所知,.NET 3.5 SP1 JIT编译器比旧版本更高效。此外,x64 JIT优化器比x86更具攻击性。 - Mehrdad Afshari
顺便说一下,“直接”的C#翻译很重要。你确定你正在启用优化检查JIT生成的汇编代码吗? - Mehrdad Afshari
请参考以下相关问题:https://dev59.com/TXNA5IYBdhLWcg3wpfiu - Dirk Vollmar
7个回答

18

我认为你在这段代码中的主要问题将是对数组进行边界检查。

如果你在C#中切换到使用不安全代码,并使用指针运算,你应该能够实现相同的(或潜在地更快的)代码。

这个问题曾在这个问题中详细讨论过


13

我认为你正在看到数组边界检查的结果。您可以通过使用不安全的代码来避免边界检查。

我相信JIT编译器可以识别像for循环这样的模式,避免边界检查,但似乎您的代码无法利用它。


9
我经常看到一些使用玩具代码进行苹果和橙子式的“相同代码”性能比较的尝试。然而,我从未看到过使用具有可比质量的完整、产品级别的代码进行的负面比较。也许是因为C#实际上并不慢。 - Greg D
1
@Greg D:我同意。我几乎完全从事高性能、科学导向的数值处理。C#的性能特征与C++有很大不同,因此分析是至关重要的——但总的来说,通过正确的分析和代码调整,你可以让C#的速度与C++一样快。 - Reed Copsey
2
@Greg, Reed - 我看到的大多数托管代码性能问题不是像这样的 CPU 时间,而是诸如加载时间和内存占用之类的问题。对于这些问题,C++ 仍然具有巨大优势(尽管糟糕的程序员很容易抵消这种优势 :)) - Michael
@Michael:没错。特别是在托管环境中,启动时间往往受到影响。 32位的内存限制是另一个问题,托管不总是能够达到本机的水平(托管通常会限制每个进程的内存使用量为1.2-1.4GB,尽管紧凑型GC在大多数情况下可以弥补这一点)。 - Reed Copsey

6

正如其他人所说,其中一个方面是边界检查。另外,你的代码在数组访问方面存在一些冗余。我通过将内部块更改为以下内容,已经成功地提高了性能:

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秒。


@Jon:也许我有什么地方没注意到,但我测不出你的版本和原始帖子中的版本之间有任何显著的性能差异。事实上,我也不认为这样一个相当微小的改变会对性能产生如此大的影响。 - Dirk Vollmar
我也不是特别想这样做,但它确实对我有用,在.NET 3.5和4.0b1上都是如此。使用/o+ /debug-编译为32位Vista控制台应用程序。我还更改了i和k变量的范围,但我怀疑那并不重要。 - Jon Skeet
我已经测试了足够多的次数,以确保这不仅仅是偶然情况 :) - Jon Skeet
@Jon:“我还改变了i和k变量的范围,但我怀疑这并不重要。” 我检查了一下,似乎对i和k进行有限的作用域实际上是性能提升的原因。如果i和k仅在for循环中是局部的,那么优化器可能能够删除边界检查,因为它可以确定i和k始终在数组的边界内(我在XP/.NET 3.5上检查了这一点)。 - Dirk Vollmar
但问题并不仅仅是这样 - 当我开始只改变作用域时,没有任何变化 - 实施答案中指定的更改确实产生了巨大的影响。我想这两个因素共同起作用。 - Jon Skeet

4

为了好玩,我尝试在Visual Studio 2010中使用C#构建它,并查看了JIT的反汇编:

                    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中对抖动进行了许多改进。


2

C#正在进行边界检查

在运行C#不安全代码的计算部分时,它是否能像本地实现一样高效?


1

如果你的应用程序的性能关键路径完全由未经检查的数组处理组成,我建议你不要将其重写为C#。

但是,如果你的应用程序在语言X中已经运行良好,我建议你不要将其重写为语言Y。

你想从重写中获得什么?至少要认真考虑使用混合语言解决方案,利用你已经调试过的C代码来处理高性能部分,并使用C#来获得漂亮的用户界面或方便地集成最新的丰富.NET库。

可能相关主题的更长答案。


0
我确定 C 语言的优化和 C# 不同。此外,你必须预期至少会有一点性能下降。.NET 框架为应用程序增加了另一层。
这种折衷方案是更快速的开发、庞大的库和函数,代价是(本应)小幅度的速度下降。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接