警告:您提出的问题确实非常复杂-可能比您意识到的要复杂得多。结果,这是一个很长的答案。
从纯粹的理论观点来看,可能对此有一个简单的答案:(可能)C#并没有真正阻止其像C ++一样快的速度。尽管有理论,但是有一些实际的原因,即在某些情况下某些情况下它的运行速度较慢。
我将考虑差异的三个基本方面:语言功能,虚拟机执行和垃圾回收。后两者经常一起使用,但是可以是独立的,因此我将分别对其进行研究。
语言特征
C ++非常重视模板,并且模板系统中的功能主要旨在在编译时尽可能多地完成功能,因此从程序的角度来看,它们是“静态的”。模板元编程允许在编译时执行完全任意的计算(即,模板系统已完成Turing)。这样,基本上任何不依赖用户输入的内容都可以在编译时进行计算,因此在运行时它只是一个常数。但是,对此的输入可能包括类型信息之类的内容,因此,您通常会在编译时通过C ++中的模板元编程在C#的运行时通过反射来完成很多工作。不过,在运行时速度和多功能性之间绝对需要权衡取舍-模板可以做什么,
语言功能上的差异意味着几乎所有通过将某些C#转换为C ++来进行两种语言比较的尝试(反之亦然)很可能会在无意义和误导性之间产生结果(对于大多数其他语言对也是如此)以及)。一个简单的事实是,对于任何大于几行代码的东西,几乎没有人可能会以相同的方式(或足够接近的相同方式)使用这些语言,这样的比较可以告诉您有关这些语言的信息在现实生活中工作。
虚拟机
与几乎任何合理的现代VM一样,Microsoft的.NET可以也将进行JIT(又称“动态”)编译。但是,这代表了许多权衡。
首先,优化代码(像大多数其他优化问题一样)在很大程度上是NP完全问题。对于除了真正的琐碎/玩具程序以外的任何事情,您几乎都可以保证不会真正地“优化”结果(即,您找不到真正的最优值)-优化器只会使代码比它更好以前是。但是,许多众所周知的优化都需要花费大量时间(通常是内存)来执行。使用JIT编译器时,用户正在等待编译器运行。排除了大多数较昂贵的优化技术。静态编译具有两个优点:首先,如果它很慢(例如,构建大型系统),通常是在服务器上执行的,而没有人花时间等待。其次,可执行文件只能生成一次,并被许多人使用多次。首先是将优化成本降至最低;第二种方法则是在大量执行中分摊出较小的成本。
如原始问题(和许多其他网站)所述,JIT编译确实有可能提高对目标环境的了解,这应该(至少在理论上)抵消了这一优势。毫无疑问,该因素可以抵消至少部分静态编译的缺点。对于一些相当特定类型的代码和目标环境,它可以甚至超过了静态编译的优势,有时甚至是巨大的优势。至少从我的测试和经验来看,这是非常不寻常的。取决于目标的优化通常似乎只产生很小的差异,或者只能(无论如何自动地)应用于相当具体的问题类型。显然,如果您在现代计算机上运行相对较旧的程序,就会发生这种情况。用C ++编写的旧程序可能已被编译为32位代码,并且即使在现代64位处理器上也将继续使用32位代码。用C#编写的程序将被编译为字节码,然后VM将其编译为64位机器码。如果该程序从以64位代码运行中获得了实质性的好处,那将带来巨大的好处。在64位处理器相当新的短时间内,这种情况发生了很多。不过,通常有可能会受益于64位处理器的最新代码通常可以静态编译为64位代码。
使用虚拟机还可以改善缓存使用率。VM的指令通常比本机指令更紧凑。在给定数量的缓存中可以容纳更多的代码,因此您有更大的机会在需要时将任何给定的代码放入缓存。这可以帮助使解释后的VM代码执行(就速度而言)比大多数人最初期望的更具竞争力-您可以在一个高速缓存未命中的时间内在现代CPU上执行许多指令。
还值得一提的是,这两个因素之间不一定完全不同。没有什么可以阻止(例如)C ++编译器产生旨在在虚拟机上运行(带有或不带有JIT)的输出的。实际上,Microsoft的C ++ / CLI几乎可以满足要求-一个(几乎)符合标准的C ++编译器(尽管有很多扩展),可以生成旨在在虚拟机上运行的输出。
反之亦然:Microsoft现在拥有.NET Native,它将C#(或VB.NET)代码编译为本地可执行文件。这样提供的性能通常更像C ++,但是保留了C#/ VB的功能(例如,编译为本机代码的C#仍支持反射)。如果您有性能密集的C#代码,这可能会有所帮助。
垃圾收集
从我所看到的,我想说垃圾回收是这三个因素中最难理解的。仅举一个显而易见的例子,这里的问题提到:“除非您创建并销毁了数千个对象,否则GC也不会增加很多开销。” 实际上,如果您创建并销毁了数千个对象,则垃圾回收的开销通常将非常低。.NET使用分代清除器,它是各种复制收集器。垃圾收集器的工作原理是从指针/参考“地点”(例如,寄存器和执行堆栈)开始公知的易于访问。然后,它“追逐”那些指向已在堆上分配的对象的指针。它检查这些对象是否有进一步的指针/引用,直到将所有对象都跟随到任何链的末端,并找到(至少潜在地)可访问的所有对象。在下一步中,它将使用(或至少可能正在使用)所有对象,并通过将所有对象复制到堆中要管理的内存一端的连续块中来压缩堆。然后剩下的内存是空闲的(必须运行模数终结器,但至少在编写良好的代码中,它们很少见,因此我暂时将其忽略)。
这意味着如果创建和销毁大量对象,则垃圾回收只会增加很少的开销。由垃圾回收周期所花费的时间几乎完全依赖于已创建的,但对象的数量不被破坏。匆忙创建和销毁对象的主要结果是,GC必须更频繁地运行,但是每个周期仍然会很快。如果创建对象但不销毁它们,GC将运行更频繁,并且每个周期将大大降低,因为它将花费更多的时间来追踪指向可能存在的对象的指针,并花费更多的时间来复制仍在使用的对象。
为了解决这个问题,世代清除工作的前提是,已经“存活”了很长时间的对象很可能会继续存活更长的时间。基于此,它有一个系统,可以使在一定数量的垃圾回收周期中保留下来的对象得到“保留”,并且垃圾回收器开始简单地假设它们仍在使用中,因此,与其在每个周期进行复制,不如将其保留下来。他们一个人。这通常是一个有效的假设,以致世代清除通常比大多数其他形式的GC的开销要低得多。
人们对“手动”内存管理的了解也很少。仅举一个例子,许多比较尝试都假定所有手动内存管理也都遵循一种特定的模型(例如,最佳匹配分配)。与许多人关于垃圾收集的信念(例如,普遍认为通常使用引用计数完成)相比,这通常离现实更近(如果有的话)。
考虑到垃圾回收和手动内存管理的策略多种多样,就整体速度而言,很难将两者进行比较。试图比较分配和/或释放内存的速度(本身)几乎可以保证产生的结果充其量是没有意义的,而在最坏的情况下则完全是误导性的。
奖励主题:基准
由于相当多的博客,网站,杂志文章等声称可以在一个方向或另一个方向上提供“客观”证据,因此我也会在这个问题上投入2美分的价值。
这些基准中的大多数都有点像青少年决定比赛自己的赛车,而无论谁获胜都可以保留两辆赛车。但是,网站在一个关键方面有所不同:发布基准的人可以同时驾驶两辆车。偶然地,他的车总是赢了,其他人都不得不满足于“相信我,我真的在尽你所能地开车。”
编写糟糕的基准测试很容易,它产生的结果几乎没有意义。几乎任何人都具备设计基准的必要技能,该基准可以产生有意义的结果,并且也具有产生可以给出他想要的结果的基准的技能。实际上,编写代码以产生特定的结果可能比真正产生有意义的结果的代码容易。
正如我的朋友詹姆士·坎泽(James Kanze)所说,“永远不要相信基准并不会伪造自己。”
结论
没有简单的答案。我可以肯定地说,我可以掷硬币来选择获胜者,然后选择(说)1到20之间的数字作为获胜的百分比,并编写一些看起来像一个合理且公平的基准的代码,并且得出了已定的结论(至少在某些目标处理器上-不同的处理器可能会稍微改变百分比)。
正如其他人指出的那样,对于大多数代码而言,速度几乎是无关紧要的。推论到(这是更经常被忽略)是在小代码中速度非常重要,它通常是相当重要的很多。至少根据我的经验,对于真正重要的代码,C ++几乎总是赢家。肯定有一些有利于C#的因素,但是在实践中,它们似乎比那些有利于C ++的因素所抵消。您当然可以找到表明您选择结果的基准,但是当您编写真实代码时,几乎总是可以在C ++中使其比在C#中更快。可能会(或可能不会)花费更多的技巧和/或精力来编写,但是几乎总是可能的。