C#与C之间的性能差异很大。

100

我发现在C和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秒才能运行。 C是使用mingw编译为Windows可执行文件的。 我一直以为C/C++比C#.net更快或者至少可以媲美。到底是什么原因导致C代码运行速度变慢了30倍以上? 编辑: 似乎C#优化器删除了未被使用的root。我将root赋值改为root +=并在最后打印总数。 我还使用了带有/O2标志的cl.exe编译了C,以达到最大速度。 现在的结果是: C为3.75秒 C#为2.61秒 C仍然需要更长时间,但这是可以接受的。

21
建议您使用StopWatch而非仅使用DateTime。 - Alex Fort
3
哪些编译器标志?它们都启用了优化编译吗? - jalf
2
当您在C++编译器中使用-ffast-math呢? - Dan McClain
12
多么有趣的问题! - Robert S.
4
也许C语言中的sqrt函数不如C#中的好用。这并不是C语言本身的问题,而是与其相关联的库的问题。尝试在没有数学函数的情况下进行一些计算。 - klew
显示剩余7条评论
13个回答

172

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

Time elapsed: 0.000000
如果你不启用优化,那么你所做的任何基准测试都是完全没有价值的。(如果你启用了优化,循环会被优化掉。所以你的基准测试代码也是有缺陷的。你需要强制让它运行循环,通常是通过汇总结果或类似的方式,并在最后打印出来。)
看起来你正在测量的基本上是“哪个编译器插入了最多的调试开销”。结果发现答案是C语言。但这并不能告诉我们哪个程序最快。因为当你想获得速度时,你会启用优化。
顺便说一句,在长期运行中如果你放弃任何一种语言比其他语言更“快”的概念,你将节省自己很多麻烦。C#和英语一样并没有速度。
在C语言中,有些东西即使在一个天真的、没有进行优化的编译器中也是有效的,而有些则严重依赖于编译器对所有东西进行优化。当然,对于C#或任何其他语言也是如此。
执行速度由以下因素决定:
- 你运行的平台(操作系统、硬件、在系统上运行的其他软件) - 编译器 - 你的源代码
一个好的C#编译器会产生高效的代码。一个糟糕的C编译器会生成慢速的代码。那么一个生成C#代码的C编译器呢,你可以通过C#编译器运行它。它会跑多快?语言没有速度,你的代码才有。

这里有更多有趣的阅读内容:http://blogs.msdn.com/ricom/archive/2005/05/10/416151.aspx - Daniel Earwicker
22
回答不错,但我不同意关于语言速度的观点,至少在类比中:研究发现,威尔士语是一种较慢的语言,因为长元音的高频率。此外,如果单词的发音速度更快,人们记忆单词(和单词列表)会更好。 参考文献链接: http://web.missouri.edu/~cowann/docs/articles/before%201993/Cowan%20et%20al%20JML%201992%20verbal%20output%20time.pdf http://en.wikipedia.org/wiki/Vowel_length http://en.wikipedia.org/wiki/Welsh_language - exceptionerror
1
这不取决于你在Welsch中说了什么吗?我觉得不太可能所有的东西都会变慢。 - jalf
5
各位,不要跑题。如果同一个程序在不同的语言中运行速度不同,那是因为生成了不同的汇编代码。在这个特定的例子中,99%或更多的时间将用于浮点数i和sqrt函数,因此这就是被测量的内容。 - Mike Dunlavey

124

我简要地介绍一下,这个问题已经被回答了。C# 的一个巨大优势是具有一个良好定义的浮点数模型,恰好匹配 x86 和 x64 处理器上 FPU 和 SSE 指令集的本地操作模式。JIT 编译器会将 Math.Sqrt() 编译为几条内联指令。

而原生的 C/C++ 由于需要向后兼容多年,因此有 /fp:precise、/fp:fast 和 /fp:strict 等编译选项,它必须调用实现 sqrt() 并检查所选浮点选项以调整结果的 CRT 函数,从而导致速度变慢。


5
无需调用任何crt函数即可使用fsqrt。只需要使用--fast-math或内联汇编即可。本地代码始终比“托管代码”更快,但有时需要告诉编译器该怎么做。 - user877329
71
这是C++程序员中的一种奇怪信念,他们似乎认为C#生成的机器码与本地编译器生成的机器码有所不同。实际上它们只有一种。无论你使用什么gcc编译器开关或者写入内联汇编,FSQRT指令只有一种。它并不总是因为由本地语言生成而更快,cpu并不在意。 - Hans Passant
18
这就是使用 ngen.exe 进行预编译的解决方法。我们正在谈论的是 C#,而不是 Java。 - Hans Passant
10
不,x64的即时编译器使用SSE。 Math.Sqrt()会被转换成sqrtsd机器码指令。 - Hans Passant
6
虽然这并不是语言之间的区别,但与典型的 C/C++ 编译器相比,.net JITter 进行的优化要有限得多。最大的限制之一是缺乏 SIMD 支持,导致代码通常会慢大约 4 倍。不暴露许多内部函数也可能是一个很大的缺点,但这在很大程度上取决于你所做的事情。 - CodesInChaos
显示剩余4条评论

63

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

您可以尝试将平方根值累加到累加器中,在方法结束时将其打印出来,看看发生了什么。

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


1
一些小的实验表明这并不是事实。尽管循环的代码被生成,但也许运行时足够聪明以跳过它。即使是累加,C#仍然比C更出色。 - Dana
3
问题似乎出在另一端。在所有情况下,C# 都表现得合理。他的 C 代码显然是没有进行优化编译的。 - jalf
2
很多人都没有理解重点。我已经阅读了许多类似的案例,其中C#优于C/C++,而反驳总是要使用一些专家级别的优化技术。99%的程序员没有使用这种优化技术的知识,只是为了使他们的代码比C#代码稍微快一点而已。C/C++的用例正在减少。 - user2074102

60

我是一名C++和C#开发者。我从.NET框架的第一个beta版开始就一直在开发C#应用程序,且有20年以上的C++应用程序开发经验。首先,C#代码永远不会比C++应用程序更快,但我不会详细讨论托管代码、它是如何工作的、互操作层、内存管理内部、动态类型系统和垃圾收集器。然而,让我继续说,在这里列出的基准测试都产生了错误的结果。

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

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

优化编译器后,C++应用程序只需0.72秒,C#应用程序需要1.16秒(均为发布构建),因为C#应用程序非常基本,循环中使用的内存在堆栈上分配而不是在堆上分配,所以实际上比涉及对象、大量计算和更大数据集的真实应用程序表现得要好得多。因此提供的数字是偏向C#和.NET框架的乐观数字。即使有这种偏向,C++应用程序完成的时间也只是等效的C#应用程序的一半多一点。请记住,我使用的Microsoft C++编译器没有正确的管道和超线程优化(使用WinDBG查看汇编指令)。

如果我们使用英特尔编译器(顺便说一下,这是在AMD /英特尔处理器上生成高性能应用程序的行业机密),则相同的代码对于C++可执行文件的执行时间为0.54秒,而使用Microsoft Visual Studio 2010则为0.72秒。因此,最终结果是C++的0.54秒和C#的1.16秒。因此,由.NET JIT编译器生成的代码需要比C++可执行文件花费214%的时间。在0.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(使用自定义操作)确定。

27
  1. Microsoft从未声称C#更快,他们声称它的速度大约是原来的90%,更快地开发(因此有更多时间进行调优)并且由于内存和类型安全性更少出现错误。所有这些都是真实的(我有20年C++和10年C#的经验)。
  2. 在大多数情况下,启动性能是没有意义的。
  3. 还有更快的C#编译器,如LLVM(因此拿出英特尔和苹果公司不是一样的)。
- ben
13
创业公司的性能表现并非无意义,在大多数企业基于Web的应用程序中都非常重要,这就是为什么微软在.NET 4.0中引入了预加载(自动启动)Web页面。当应用程序池定期回收时,每个页面的首次加载会对复杂页面造成显著延迟,并导致浏览器超时。 - Richard
8
微软在早期的营销材料中声称.NET的性能更快。他们还声称垃圾收集器对性能影响很小或没有影响。这些声明中的一些出现在早期版本的ASP.NET和.NET图书中。虽然微软没有明确表示您的C#应用程序比您的C++应用程序更快,但他们可能会做出概括性的评论和营销口号,例如“即时编译意味着运行速度快”(http://msdn.microsoft.com/en-us/library/ms973894.aspx)。 - Richard
79
-1,这段怒斥充斥着不正确且误导性的陈述,比如显眼的谎言“C# 代码永远无法比 C++ 应用程序更快”。 - BCoates
36
你应该阅读Rico Mariani与Raymond Chen关于C#与C性能对比的较量:http://blogs.msdn.com/b/ricom/archive/2005/05/16/418051.aspx。简而言之:即使由微软最聪明的人进行了大量优化,也需要花费很多精力才能让C版本比一个简单的C#版本更快。 - Rolf Bjarne Kvinge
显示剩余3条评论

10

我的第一个猜测是编译器进行了优化,因为你从未使用过root。你只是分配了它,然后一遍又一遍地覆盖它。

编辑:该死,被赶上了,比先前回答晚了9秒!


2
我说你是正确的。实际变量被覆盖后再也没有被使用过。CSC 很可能会放弃整个循环,而 C++ 编译器可能会将其保留。更准确的测试方法是累加结果,然后在最后打印出该结果。此外,不应该硬编码种子值,而应该让用户定义它。这样就不会给 C# 编译器留下任何余地。 - user922475

7

要查看循环是否被优化掉,请尝试更改您的代码为

root += Math.Sqrt(i);

同样地,在C代码中计算,然后在循环外打印出根的值。

6
也许C#编译器注意到您没有在任何地方使用root,因此它跳过了整个for循环。 :)
这可能不是原因,但我怀疑无论原因是什么,都取决于编译器的实现。尝试使用Microsoft编译器(cl.exe,作为win32 sdk的一部分提供)以优化和发布模式编译您的C程序。我打赌您会看到与其他编译器相比的性能提高。
编辑:我不认为编译器可以仅通过优化for循环来进行优化,因为它必须知道Math.Sqrt()没有任何副作用。

2
@Neil,@jeff:同意,它可以很容易地知道。根据实现方式,对Math.Sqrt()的静态分析可能并不那么困难,尽管我不确定具体执行了哪些优化。 - John Feminella

6

根据您的代码,我编写了两个类似的测试程序,分别使用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运行时不应该允许cull sqrt操作),尽管数组要小得多(不想使用过多内存)。我在发布配置中编译了这些测试,并从控制台窗口内运行它们(而不是通过VS启动)。

在我的电脑上,C#程序的运行时间在6.2秒至6.9秒之间变化,而C语言版本的运行时间在6.9秒至7.1秒之间变化。


5
无论时间差有多少,都不能视为“经过的时间”。只有在两个程序运行条件完全相同的情况下,才能算作有效时间。也许你可以尝试使用win.等效命令来运行"$ /usr/bin/time my_cprog;/usr/bin/time my_csprog"。

1
为什么这个被踩了?有人认为中断和上下文切换不会影响性能吗?有人可以对TLB缺失、页面交换等进行假设吗? - Tom

5

如果您只在汇编语言级别单步执行代码,包括跟踪平方根例程,那么您很可能会得到您的问题的答案。

不需要进行教育猜测。


我想知道如何做到这一点。 - Josh Stodola
取决于您的IDE或调试器。在程序开始处中断。显示反汇编窗口并开始单步执行。如果使用GDB,则有逐个指令跳转的命令。 - Mike Dunlavey
现在这是一个好的提示,它帮助我们更好地理解下面实际发生了什么。这是否也显示了像内联和尾调用这样的JIT优化? - gjvdkamp
FYI:对我来说,这显示了VC++使用fadd和fsqrt,而C#使用cvtsi2sd和sqrtsd,据我所知,这些是SSE2指令,在支持的情况下速度要快得多。 - danio

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