C#vs C-巨大的性能差异


94

我发现C anc C#中相似代码之间存在巨大的性能差异。

C代码是:

#include <stdio.h>
#include <time.h>
#include <math.h>

main()
{
    int i;
    double root;

    clock_t start = clock();
    for (i = 0 ; i <= 100000000; i++){
        root = sqrt(i);
    }
    printf("Time elapsed: %f\n", ((double)clock() - start) / CLOCKS_PER_SEC);   

}

C#(控制台应用程序)为:

using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            DateTime startTime = DateTime.Now;
            double root;
            for (int i = 0; i <= 100000000; i++)
            {
                root = Math.Sqrt(i);
            }
            TimeSpan runTime = DateTime.Now - startTime;
            Console.WriteLine("Time elapsed: " + Convert.ToString(runTime.TotalMilliseconds/1000));
        }
    }
}

使用以上代码,C#将在0.328125秒(发行版)中完成,并且C需要11.14秒才能运行。

使用mingw将c编译为Windows可执行文件。

我一直认为C / C ++更快或更至少可以与C#.net相提并论。是什么原因导致C的运行速度慢了30倍?

编辑:似乎C#优化器正在删除根,因为它没有被使用。我将根分配更改为根+ =并在最后打印出总数。我还使用cl.exe编译了C,并将/ O2标志设置为最大速度。

现在的结果是:C为3.75秒,C#为2.61秒

C仍然需要更长的时间,但这是可以接受的


18
我建议您使用秒表,而不只是DateTime。
亚历克斯·福特

2
哪些编译器标志?是否都启用了优化编译功能?
jalf

2
在C ++编译器中使用-ffast-math怎么办?
丹·麦克莱恩

10
多么有趣的问题!
罗伯特·S。2009年

4
也许C sqrt函数在C#中不如此。这样,C便不是问题,而是附加了库。尝试一些没有数学功能的计算。
klew

Answers:


61

由于您从不使用“ root”,因此编译器可能已删除了用于优化方法的调用。

您可以尝试将平方根值累加到一个累加器中,在方法末尾将其打印出来,然后看看发生了什么。

编辑:请参阅下面的Jalf的答案


1
进行一些实验表明情况并非如此。生成了循环代码,尽管也许运行时足够聪明以至于可以跳过它。即使累积,C#仍然击败了C。–
Dana

3
看来问题出在另一端。C#在所有情况下都表现合理。他的C代码显然没有优化编译
jalf

2
你们中的很多人都没有在这里讲到重点。我读过很多类似的案例,其中c#的性能优于c / c ++,并且总是反驳是采用专家级的优化。99%的程序员不具备使用这种优化技术的知识,仅仅是为了使其代码比c#代码运行得更快。C / C ++的用例正在缩小。

167

您必须比较调试版本。我刚刚编译了您的C代码,并得到了

Time elapsed: 0.000000

如果您不启用优化,那么您进行的任何基准测试都是毫无价值的。(如果您启用了优化功能,则循环将被优化。因此,您的基准测试代码也存在缺陷。您需要强制其运行循环,通常是对结果或类似结果求和,然后最后将其打印出来)

看来您要衡量的基本上是“哪个编译器插入了最多的调试开销”。答案是C。但这并不能告诉我们哪个程序最快。因为当您需要速度时,您可以启用优化。

顺便说一句,从长远来看,如果您放弃任何一种比其他语言“更快”的语言,就可以避免很多麻烦。C#的速度不比英语快。

即使在天真的非优化编译器中,C语言中的某些功能仍然有效,还有其他功能则严重依赖于编译器来优化所有内容。当然,C#或任何其他语言也是如此。

执行速度取决于:

  • 您运行的平台(操作系统,硬件,系统上运行的其他软件)
  • 编译器
  • 您的源代码

一个好的C#编译器将产生有效的代码。错误的C编译器将生成缓慢的代码。生成C#代码的C编译器怎么样,然后可以通过C#编译器运行它?那将运行多快?语言没有速度。您的代码可以。



18
好的答案,但至少在类比上,我对语言的速度持不同意见:由于长元音的出现频率很高,因此我们发现威尔士语比大多数语言都慢。此外,如果人们说得更快,他们会更好地记住单词(和单词列表)。web.missouri.edu/~cowann/docs/articles/before%201993/... en.wikipedia.org/wiki/Vowel_length en.wikipedia.org/wiki/Welsh_language
exceptionerror

1
但是,这不取决于您在威尔士语中所说的吗?我发现一切都变慢的可能性不大。
jalf

5
++大家好,请不要在这里盘side。如果同一程序在一种语言下的运行速度比另一种语言快,那是因为生成了不同的汇编代码。在此特定示例中,99%或更多的时间将变为i,,sqrt这就是要测量的内容。
Mike Dunlavey,2009年

116

我会保持简短,已经标记为已回答。C#具有定义明确的浮点模型的巨大优势。这恰好与x86和x64处理器上FPU和SSE指令集的本机操作模式相匹配。那里不是巧合。JITter将Math.Sqrt()编译为一些内联指令。

本机C / C ++背负着多年的向后兼容性。/ fp:precise,/ fp:fast和/ fp:strict编译选项最为明显。因此,它必须调用一个实现sqrt()的CRT函数,并检查所选浮点选项以调整结果。太慢了


66
这是C ++程序员的一种奇怪信念,他们似乎认为C#生成的机器代码与本机编译器生成的机器代码有所不同。只有一种。无论您使用哪种gcc编译器开关或编写的内联汇编,仍然只有一条FSQRT指令。它并不总是更快,因为它是由本地语言生成的,而CPU则不在乎。
汉斯·帕桑

16
这就是使用ngen.exe预先插入即可解决的问题。我们在谈论的是C#,而不是Java。
汉斯·帕桑

20
@ user877329-真的吗?哇。
安德拉斯·佐坦

7
不,x64抖动使用SSE。Math.Sqrt()转换为sqrtsd机器代码指令。
汉斯·帕桑

6
尽管从技术上来说,语言之间没有区别,但与典型的C / C ++编译器相比,.net JITter所做的优化非常有限。最大的局限之一是缺乏SIMD支持,使代码的运行速度通常降低了4倍左右。不公开很多内在函数也可能是一个大弊端,但这在很大程度上取决于您在做什么。
CodesInChaos

57

我是C ++和C#开发人员。自.NET框架的第一个beta版以来,我已经开发了C#应用程序,并且在开发C ++应用程序方面有20多年的经验。首先,C#代码永远不会比C ++应用程序快,但我不会就托管代码,其工作方式,互操作层,内存管理内部,动态类型系统和垃圾收集器进行冗长的讨论。不过,让我继续说一下这里列出的基准都会产生不正确的结果。

让我解释一下:我们需要考虑的第一件事是C#(.NET Framework 4)的JIT编译器。现在,JIT使用各种优化算法(比Visual Studio附带的默认C ++优化器更具攻击性)为CPU生成本机代码,.NET JIT编译器使用的指令集更贴近实际CPU在机器上,因此可以对机器代码进行某些替换,以减少时钟周期并提高CPU管线缓存中的命中率,并产生进一步的超线程优化,例如指令重排序和与分支预测有关的改进。

这意味着,除非为RELEASE构建(而不是DEBUG构建)使用正确的参数编译C ++应用程序,否则C ++应用程序的性能可能会比相应的基于C#或.NET的应用程序慢。在C ++应用程序上指定项目属性时,请确保启用“完全优化”和“快速收藏夹代码”。如果您拥有64位计算机,则必须指定将x64生成为目标平台,否则您的代码将通过转换子层(WOW64)执行,这将大大降低性能。

在编译器中执行正确的优化后,对于C ++应用程序,我将获得0.72秒,对于C#应用程序,我将获得1.16秒(均在发行版中)。由于C#应用程序非常基础,并且在堆栈上而不是在堆上分配循环中使用的内存,因此它的性能实际上要好于涉及对象,繁重的计算和较大数据集的真实应用程序。因此,提供的数据是偏向C#和.NET框架的乐观数据。即使存在这种偏差,C ++应用程序的完成时间也仅是等效C#应用程序的一半以上。请记住,我使用的Microsoft C ++编译器没有正确的管道和超线程优化(使用WinDBG查看汇编指令)。

现在,如果我们使用Intel编译器(顺便说一句,这是在AMD / Intel处理器上生成高性能应用程序的工业秘密),则对于C ++可执行文件,相同的代码将在.54秒内执行,而使用Microsoft Visual Studio 2010则为.72秒因此,最终结果是C ++为0.54秒,C#为1.16秒。因此,.NET JIT编译器生成的代码比C ++可执行文件花费214%的时间。在.54秒内花费的大部分时间是从系统获取时间,而不是从循环本身获取时间!

统计信息中还缺少启动和清除时间,这些时间未包括在计时中。与C ++应用程序相比,C#应用程序倾向于在启动和终止上花费更多的时间。其背后的原因很复杂,并且与.NET运行时代码验证例程和内存管理子系统有关,后者在程序的开头(因此,最后)执行了大量工作以优化内存分配和垃圾集电极。

在测量C ++和.NET IL的性能时,重要的是要查看汇编代码,以确保所有计算都在那里。我发现,没有在C#中添加一些其他代码,实际上上述示例中的大多数代码已从二进制文件中删除。当您使用更具侵略性的优化器(例如Intel C ++编译器随附的优化器)时,C ++也是如此。我上面提供的结果是100%正确的,并且在装配级别得到了验证。

互联网上许多论坛的主要问题在于,许多新手在听不懂技术的情况下就听微软的营销宣传,并错误地声称C#比C ++快。从理论上讲,C#比C ++更快,因为JIT编译器可以为CPU优化代码。这种理论的问题在于,.NET框架中存在大量管道,这会降低性能。C ++应用程序中不存在的管道。此外,有经验的开发人员将知道适用于给定平台的正确编译器,并在编译应用程序时使用适当的标志。在Linux或开放源代码平台上,这不是问题,因为您可以分发源代码并创建使用适当的优化来编译代码的安装脚本。在Windows或封闭源代码平台上,您将必须分发多个可执行文件,每个可执行文件都有特定的优化。将要部署的Windows二进制文件基于msi安装程序检测到的CPU(使用自定义操作)。


22
1.微软从来没有说过C#速度更快的说法,因为内存和类型安全性,他们声称其速度约为90%,开发速度更快(因此有更多的时间进行调整),而且没有更多的错误。所有这些都是正确的(我在C ++中有20年,在C#中有10年)。2在大多数情况下,启动性能毫无意义。3.也有更快的C#编译器像LLVM(所以带来了英特尔不是苹果对苹果)

13
启动性能并非毫无意义。在大多数基于企业Web的应用程序中,这非常重要,这就是Microsoft为什么在.NET 4.0中引入要预加载(自动启动)的网页的原因。当应用程序池每隔一段时间被回收一次时,第一次加载每个页面将增加复杂页面的显着延迟,并导致浏览器超时。
理查德

8
微软声称,在早期的市场营销资料中,.NET的性能更快。他们还声称垃圾收集器对性能几乎没有影响。这些主张中的一些使它成为了早期版本中的各种书籍(在ASP.NET和.NET上)。尽管Microsoft并未明确表示您的C#应用​​程序将比您的C ++应用程序快,但它们确实会笼统地注释和营销口号,例如“即时即刻运行”(msdn.microsoft.com/ zh-cn / library / ms973894.aspx)。
理查德

71
-1,这句话充斥着不正确和误导性的陈述,例如明显的废话“ C#代码永远不会比C ++应用程序快”
BCoates

32
-1。您应该阅读Rico Mariani vs Raymond Chen的C#vs C性能之战:blogs.msdn.com/b/ricom/archive/2005/05/16/418051.aspx。简而言之:Microsoft最聪明的人之一做了很多优化工作,以使C版本比简单的C#版本更快。
Rolf Bjarne Kvinge 2012年

10

我的第一个猜测是编译器优化,因为您从不使用root。您只需分配它,然后一次又一次覆盖它。

编辑:该死,被9秒击败!


2
我说你是对的。实际变量将被覆盖,并且在此之后再也不会使用。csc很可能会放弃整个循环,而c ++编译器可能会将其留在其中。更准确的测试是累积结果,然后最后打印出结果。同样,不应该对种子值进行硬编码,而应由用户定义。这将不会给c#编译器留下任何余地。

7

要查看循环是否已被优化,请尝试将代码更改为

root += Math.Sqrt(i);

在C代码中也是如此,然后在循环外打印root的值。


6

也许C#编译器注意到您在任何地方都不使用root,所以它只是跳过了整个for循环。:)

可能并非如此,但我怀疑原因是什么,这取决于编译器实现。尝试使用具有优化和发布模式的Microsoft编译器(cl.exe,作为win32 sdk的一部分提供)编译C程序。我敢打赌,与其他编译器相比,您会看到性能方面的改进。

编辑:我不认为编译器可以优化for循环,因为它必须知道Math.Sqrt()没有任何副作用。


2
也许它知道这一点。

2
@ Neil,@ jeff:同意,这很容易就能知道。根据实现的不同,尽管我不确定具体执行哪些优化,但对Math.Sqrt()进行静态分析可能并不难。
John Feminella 09年

5

无论什么时候。可能是“经过的时间”无效。仅当您可以保证两个程序在完全相同的条件下运行时,它才是有效的程序。

也许您应该尝试赢球。相当于$ / usr / bin / time my_cprog; / usr / bin / time my_csprog


1
为什么这样投票?是否有人假设中断和上下文切换不会影响性能?任何人都可以对TLB丢失,页面交换等做出假设吗?
汤姆(Tom)2009年

5

我(根据您的代码)将C和C#中的另外两个可比较的测试放在一起。这两人使用模数运算符编写了一个较小的数组进行索引(这增加了一些开销,但是,我们正在尝试比较性能[粗略])。

C代码:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <math.h>

void main()
{
    int count = (int)1e8;
    int subcount = 1000;
    double* roots = (double*)malloc(sizeof(double) * subcount);
    clock_t start = clock();
    for (int i = 0 ; i < count; i++)
    {
        roots[i % subcount] = sqrt((double)i);
    }
    clock_t end = clock();
    double length = ((double)end - start) / CLOCKS_PER_SEC;
    printf("Time elapsed: %f\n", length);
}

在C#中:

using System;

namespace CsPerfTest
{
    class Program
    {
        static void Main(string[] args)
        {
            int count = (int)1e8;
            int subcount = 1000;
            double[] roots = new double[subcount];
            DateTime startTime = DateTime.Now;
            for (int i = 0; i < count; i++)
            {
                roots[i % subcount] = Math.Sqrt(i);
            }
            TimeSpan runTime = DateTime.Now - startTime;
            Console.WriteLine("Time elapsed: " + Convert.ToString(runTime.TotalMilliseconds / 1000));
        }
    }
}

这些测试将数据写入数组(因此,不应允许.NET运行时删除sqrt op),尽管该数组要小得多(不想使用过多的内存)。我在发布配置中编译了这些文件,并从控制台窗口中运行它们(而不是通过VS开始)。

在我的计算机上,C#程序在6.2到6.9秒之间变化,而C版本在6.9到7.1之间变化。


5

如果仅在汇编级别单步执行代码(包括单步执行平方根例程),则可能会找到问题的答案。

无需进行有根据的猜测。


我想知道该怎么做
Josh Stodola

取决于您的IDE或调试器。在pgm开头中断。显示反汇编窗口,然后开始单步执行。如果使用GDB,则有一些命令可一次执行一条指令。
Mike Dunlavey,2009年

现在,这是一个很好的技巧,它可以帮助人们进一步了解那里到底发生了什么。这还会显示内联和尾部调用等JIT优化吗?
gjvdkamp 2011年

仅供参考:对我来说,这显示了使用fadd和fsqrt的VC ++,而C#使用了cvtsi2sd和sqrtsd,据我所知,这是SSE2指令,因此在受支持的情况下速度要快得多。
danio

2

此处可能引起问题的另一个因素是,C编译器针对目标处理器系列编译为通用本机代码,而编译C#代码时生成的MSIL随后被JIT编译为目标,以针对您已经完成的处理器精确定位可能的优化。因此,从C#生成的本机代码可能比C更快。


从理论上讲,是的。实际上,这几乎没有任何可测量的差异。如果幸运的话,大概是一两个百分点。
jalf

或-如果您的某些类型的代码使用的扩展名不在“通用”处理器的允许列表中。像上交所的味道。尝试将处理器目标设置为更高,以了解您得到的差异。
gbjbaanb

1

在我看来,这与语言本身无关,而与平方根函数的不同实现有关。


我高度怀疑不同的sqrt实现会造成如此大的差异。
亚历克斯·福特

尤其是因为即使在C#中,大多数数学函数仍然被认为对性能至关重要,因此仍然可以实现。
马修·奥莱尼克

fsqrt是IA-32处理器指令,因此如今的语言实现已无关紧要。
不确定2009年

使用调试器进入MSVC的sqrt函数。它不仅仅执行fsqrt指令,还做很多事情。
bk1e

1

伙计们,循环并没有得到优化。我编译了John的代码并检查了生成的.exe。循环的内容如下:

 IL_0005:  stloc.0
 IL_0006:  ldc.i4.0
 IL_0007:  stloc.1
 IL_0008:  br.s       IL_0016
 IL_000a:  ldloc.1
 IL_000b:  conv.r8
 IL_000c:  call       float64 [mscorlib]System.Math::Sqrt(float64)
 IL_0011:  pop
 IL_0012:  ldloc.1
 IL_0013:  ldc.i4.1
 IL_0014:  add
 IL_0015:  stloc.1
 IL_0016:  ldloc.1
 IL_0017:  ldc.i4     0x5f5e100
 IL_001c:  ble.s      IL_000a

除非运行时足够智能以意识到循环不执行并跳过循环?

编辑:将C#更改为:

 static void Main(string[] args)
 {
      DateTime startTime = DateTime.Now;
      double root = 0.0;
      for (int i = 0; i <= 100000000; i++)
      {
           root += Math.Sqrt(i);
      }
      System.Console.WriteLine(root);
      TimeSpan runTime = DateTime.Now - startTime;
      Console.WriteLine("Time elapsed: " +
          Convert.ToString(runTime.TotalMilliseconds / 1000));
 }

(在我的计算机上)经过的时间从0.047到2.17。但这仅仅是增加一亿加法运算符的开销吗?


3
查看IL并不能告诉您关于优化的更多信息,因为尽管C#编译器执行了诸如不断折叠和删除无效代码之类的工作,但是IL接管了其余工作,并在加载时完成了其余工作。
Daniel Earwicker 09年

我认为可能就是这种情况。即使强迫它工作,它仍然比C版本快9秒。(我完全没想到这一点)
Dana
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.