C # 真的比 C + + 慢吗?

我一直在思考这个问题有一段时间了。

当然,C # 中有些东西并没有针对速度进行优化,所以使用这些对象或语言调整(如 LinQ)可能会导致代码变慢。

但是如果你不使用这些调整,只是比较 C # 和 C + + 中相同的代码片段(很容易将它们转换成另一个)。真的会那么慢吗?

我看到过一些比较,显示 C # 在某些情况下可能更快,因为理论上 JIT 编译器应该实时优化代码并得到更好的结果:

管理还是非管理?

我们应该记住,JIT 编译器实时编译代码,但这是一次性开销,相同的代码(一旦达到并编译)不需要在运行时再次编译。

GC 也不会增加很多开销,除非您创建并销毁数千个对象(比如使用 String 而不是 StringBuilder)。而且在 C + + 中这样做成本也很高。

我想提到的另一点是,在。网。那个。网络平台的通信比基于托管 COM 的 DLL 要好得多。

I don't see any inherent reason why the language should be slower, and I don't really think that C# is slower than C++ (both from experience and lack of a good explanation)..

那么,用 C # 编写的同一段代码会比用 C + + 编写的同一段代码慢吗?
如果是这样,那为什么?

还有一些其他的参考文献(只是稍微谈了一下,但是没有解释为什么) :

如果 C # 比 C + + 慢,为什么还要使用它呢?

29574 次浏览

因为你不需要总是使用“最快”的语言?我开法拉利上班不是因为它更快。

C + + 在性能上总是有优势的。使用 C # ,我不需要处理内存,而且我有大量可用的资源来完成我的工作。

你需要问自己的是哪一个更节省你的时间。机器现在非常强大,你的大部分代码应该用一种能让你在最短时间内获得最大价值的语言来完成。

如果有一个核心处理在 C # 中花费了太长的时间,那么您可以构建一个 C + + 并用 C # 进行互操作。

停止考虑代码性能,开始构建价值。

C # 比 C + + 更快,写起来更快,对于执行时间来说,没有什么比剖析器更快的了。

但是 C # 没有像 C + + 那样容易接口的库。

C # 严重依赖于窗口..。

如果有更快的路径(C #) ,为什么要编写一个不需要很多 C + + 优化的小型应用程序呢?

顺便说一句,时间紧迫的应用程序不是用 C # 或 Java 编写的,主要是由于不确定何时执行垃圾收集。

在现代,应用程序或执行速度不像以前那么重要了。开发进度、正确性和健壮性是更高的优先级。应用程序的高速版本如果有很多错误、崩溃很严重或更糟糕、错过了上市或部署的机会,那么它就不是一个好的应用程序。

由于开发计划是优先事项,新的语言正在出现,加速了开发。C # 就是其中之一。C # 还通过从 C + + 中删除导致常见问题的特性来帮助提高正确性和健壮性: 指针就是一个例子。

在大多数平台上,使用 C # 开发的应用程序和使用 C + + 开发的应用程序在执行速度上的差异可以忽略不计。这是由于执行瓶颈并不依赖于语言,而是通常依赖于操作系统或 I/O。例如,如果 C + + 在5 ms 内执行一个函数,但 C # 使用2 ms,等待数据需要2秒,那么与等待数据的时间相比,在函数中花费的时间是微不足道的。

选择一种最适合开发人员、平台和项目的语言。努力实现正确性、健壮性和部署的目标。应用程序的速度应该被视为一个 bug: 对其进行优先排序,与其他 bug 进行比较,并根据需要进行修复。

警告: 你问的问题真的很复杂——可能比你意识到的要复杂得多。因此,这是一个 真的长答案。

从纯理论的角度来看,可能有一个简单的答案: (可能)没有任何关于 C # 的东西真正阻止它像 C + + 一样快。尽管有这个理论,但是,有一些实际的原因使得它在某些情况下在某些事情上比 慢。

我将考虑三个基本的区别: 语言特性、虚拟机执行和垃圾收集。后两者经常一起出现,但可以是独立的,所以我将分别看它们。

语言特色

C + + 非常强调模板和模板系统中的特性,这些特性在很大程度上是为了尽可能多地在编译时完成,所以从程序的角度来看,它们是“静态的”模板元编程允许在编译时执行完全任意的计算(例如,模板系统是图灵完成的)。因此,本质上任何不依赖于用户输入的东西都可以在编译时计算出来,所以在运行时它只是一个常量。然而,这里的输入可以包含类型信息之类的东西,所以你在运行时通过反射所做的大部分工作通常都是在编译时通过 C + + 的模板超编程完成的。在运行时速度和通用性之间肯定存在一个权衡——模板可以做什么,它们静态地做什么,但是它们不能做反射可以做的所有事情。

语言特性的差异意味着,几乎任何仅仅通过将一些 C # 音译成 C + + (反之亦然)来比较两种语言的尝试都可能产生介于无意义和误导之间的结果(对于大多数其他语言对也是如此)。一个简单的事实是,对于任何大于几行代码的东西,几乎没有人可能以同样的方式(或者足够接近同样的方式)使用这些语言,这样的比较告诉你这些语言在现实生活中是如何工作的。

虚拟机

就像几乎所有现代化的虚拟机一样,微软的目标是。NET 可以而且将会进行 JIT (又名“动态”)编译。不过,这代表了一些权衡。

首先,优化代码(像大多数其他优化问题一样)在很大程度上是一个 NP 完全问题。对于除了一个真正琐碎的/玩具程序之外的任何东西,几乎可以保证不会真正“优化”结果(也就是说,不会找到真正的最佳值)——优化器只会让代码比以前更优化。然而,很多众所周知的优化需要大量的时间(通常还有内存)来执行。使用 JIT 编译器时,用户在等待编译器运行。大多数更昂贵的优化技术被排除在外。静态编译有两个优点: 首先,如果速度很慢(例如,构建一个大型系统) ,它通常在服务器上执行,而 没人会花时间等待。其次,可执行文件可以生成 一次,并被许多人多次使用。第一种方法将优化成本最小化; 第二种方法将更小的成本分摊到更大数量的执行上。

正如在最初的问题(以及许多其他网站)中提到的,JIT 编译确实有可能提高对目标环境的认识,这应该(至少在理论上)抵消了这一优势。毫无疑问,这个因素至少可以抵消静态编译的部分缺点。对于一些相当特定类型的代码和目标环境,可以甚至超过了静态编译的优势,有时这种优势相当明显。然而,至少在我的测试和经验中,这是相当不寻常的。依赖于目标的优化大多数情况下要么只产生相当小的差异,要么只能(自动地,无论如何)应用于相当特定类型的问题。显然,如果您在一台现代机器上运行一个相对较老的程序,就会发生这种情况。用 C + + 编写的旧程序可能已经被编译成32位代码,并且即使在现代的64位处理器上也将继续使用32位代码。用 C # 编写的程序会被编译成字节码,然后虚拟机会将其编译成64位机器码。如果这个程序从64位代码运行中获得了实质性的好处,那么它将获得实质性的优势。在64位处理器相当新的一段时间内,这种情况发生了相当多。最近的代码可能受益于64位处理器,通常可以静态地编译成64位代码。

使用 VM 还有可能提高缓存的使用率。VM 的指令通常比本机指令更紧凑。它们中的更多可以容纳给定数量的缓存内存,因此在需要时,任何给定的代码都更有可能在缓存中。这有助于使 VM 代码的解释执行比大多数人最初预期的更具竞争力(就速度而言)——在 缓存丢失所花费的时间内,您可以在现代 CPU 上执行 很多指令。

值得一提的是,这个因素在两者之间没有任何差别。没有什么可以阻止(例如) C + + 编译器生成打算在虚拟机上运行的输出(不管有没有 JIT)。事实上,微软的 C + +/CLI 就是 差不多——一个(几乎)一致的 C + + 编译器(尽管有很多扩展) ,它产生的输出打算在虚拟机上运行。

反过来也是如此: 微软现在已经。NET 本机,它将 C # (或 VB.NET)代码编译为本机可执行文件。这使得性能通常更像 C + + ,但保留了 C #/VB 的特性(例如,编译成本机代码的 C # 仍然支持反射)。如果您有性能密集型的 C # 代码,这可能会有所帮助。

垃圾收集

From what I've seen, I'd say garbage collection is the poorest-understood of these three factors. Just for an obvious example, the question here mentions: "GC doesn't add a lot of overhead either, unless you create and destroy thousands of objects [...]". In reality, if you create 还有 destroy thousands of objects, the overhead from garbage collection will generally be fairly low. .NET uses a generational scavenger, which is a variety of copying collector. The garbage collector works by starting from "places" (e.g., registers and execution stack) that pointers/references are 已知 to be accessible. It then "chases" those pointers to objects that have been allocated on the heap. It examines those objects for further pointers/references, until it has followed all of them to the ends of any chains, and found all the objects that are (at least potentially) accessible. In the next step, it takes all of the objects that are (or at least 也许吧) in use, and compacts the heap by copying all of them into a contiguous chunk at one end of the memory being managed in the heap. The rest of the memory is then free (modulo finalizers having to be run, but at least in well-written code, they're rare enough that I'll ignore them for the moment).

这意味着,如果创建大量 毁灭对象,垃圾收集增加的开销非常小。垃圾收集周期所花费的时间几乎完全取决于已创建但已销毁的 没有对象的数量。匆忙创建和销毁对象的主要后果很简单,就是 GC 必须更频繁地运行,但每个周期仍然很快。如果你创建了对象并且 不要破坏了它们,那么 GC 会运行的更加频繁,每个周期 还有的运行速度会大大减慢,因为它花费了更多的时间追踪指向可能存在的对象的指针,而 还有花费了更多的时间复制仍在使用的对象。

为了解决这个问题,分代清除工作是基于这样的假设: 保持“活动”相当长一段时间的对象可能会继续保持活动相当长一段时间。基于这一点,它有一个系统,在这个系统中,存活了一定数量的垃圾收集周期的对象被“保留”,垃圾收集器开始简单地假设它们仍在使用,因此,它不会在每个周期都复制它们,而只是让它们单独存在。这通常是一个有效的假设,代清除通常比大多数其他形式的 GC 具有相当低的开销。

人们对“手动”内存管理的理解通常也很少。仅举一个例子,许多比较的尝试都假设所有的手动内存管理也遵循一个特定的模型(例如,最佳分配)。这往往比许多人对垃圾收集的看法更接近现实(如果有的话)(例如,人们普遍认为垃圾收集通常是通过引用计数完成的)。

由于垃圾收集 还有手动内存管理的策略多种多样,因此很难比较两者的总体速度。尝试比较分配和/或释放内存的速度(自己)几乎肯定会产生毫无意义的结果,在最坏的情况下完全误导。

附加话题: 基准

由于相当多的博客,网站,杂志文章等,声称提供“客观”的证据在一个或另一个方向,我会把我的两美分价值在这个问题上。

这些基准中的大多数都有点像青少年决定参加赛车比赛,谁赢了谁就可以保留这两辆车。不过,这两个网站在一个关键方面有所不同: 发布基准测试结果的人可以同时驾驶两辆车。奇怪的是,他的车总是赢,其他人不得不满足于“相信我,我是 真的驾驶你的车的最快速度。”

编写一个糟糕的基准很容易产生几乎毫无意义的结果。几乎所有具备必要技能的人都能设计出能产生任何有意义结果的基准测试,同时也具备产生他决定想要的结果的技能。实际上,可能是 更容易编写代码来产生特定的结果,而不是真正产生有意义结果的代码。

正如我的朋友詹姆斯•肯泽(James Kanze)所说,“永远不要相信没有伪造自己的基准。”

结论

没有简单的答案。我有理由相信,我可以抛硬币来选择赢家,然后在1到20之间选择一个数字来计算赢家所占的百分比,然后编写一些看起来像是合理和公平的基准的代码,并得出预定的结论(至少在某些目标处理器上——不同的处理器可能会稍微改变百分比)。

正如其他人指出的那样,对于 大部分代码,速度几乎是无关紧要的。由此得到的推论(这一点常常被忽略)是,在速度很重要的小代码中,通常 很多也很重要。至少在我的经验中,对于真正重要的代码来说,C + + 几乎总是赢家。当然有一些因素有利于 C # ,但是在实践中,这些因素似乎被有利于 C + + 的因素所压倒。您当然可以找到指示您选择的结果的基准,但是当您编写实际代码时,几乎总是可以使 C + + 比 C # 更快。写作可能需要(也可能不需要)更多的技巧和/或努力,但事实上总是有可能的。

除非您在特定的系统上执行基准测试,否则您的问题不可能得到准确的答案。然而,想想 C # 和 C + + 这样的编程语言之间的一些基本区别还是很有趣的。

汇编

执行 C # 代码需要一个额外的步骤,其中代码是 JIT 化的。关于性能,这将有利于 C + + 。此外,JIT 编译器只能优化 JIT 编译的代码单元(例如方法)中生成的代码,而 C + + 编译器可以使用更积极的技术跨方法调用进行优化。

然而,JIT 编译器能够优化生成的机器代码,使其与底层硬件紧密匹配,从而使其能够利用其他硬件特性(如果存在的话)。据我所知。NET JIT 编译器不能做到这一点,但它可以为 Atom 生成不同于 Pentium CPU 的代码。

内存访问

在许多情况下,垃圾收集架构可以创建比标准 C + + 代码更优化的内存访问模式。如果用于第一代的内存区域足够小,可以保留在 CPU 缓存中,提高性能。如果创建和销毁大量小对象,维护托管堆的开销可能会小于 C + + 运行时所需的开销。同样,这高度依赖于应用程序。性能研究 Python演示了一个特定的托管 Python 应用程序能够比编译后的版本扩展得更好,因为它具有更优化的内存访问模式。

大约在2005年,来自本地/管理围栏两侧的两位 MS 性能专家试图回答同样的问题。他们的方法和过程仍然令人着迷,结论至今仍然成立——我不知道还有什么更好的尝试来给出一个明智的答案。他们指出,关于性能差异的 潜在的原因讨论是假设性的和徒劳的,真正的讨论必须有一些关于这种差异对现实世界的影响的 经验基础

因此,旧新陈雷蒙德Rico Mariani为友谊赛制定了规则。选择中文/英文字典作为玩具应用程序上下文: 简单到可以作为业余项目进行编码,但复杂到可以演示非平凡的数据使用模式。规则一开始很简单—— Raymond 编写了一个简单的 C + + 实现,Rico 将其迁移到 C # 一行一行,没有任何复杂性,两个实现都运行了基准测试。随后,进行了几次优化迭代。

详细信息在这里: 1234567891020、 21、 22、 23。

这段关于巨人的对话非常有教育意义,我衷心建议大家投入进去——但是如果你没有时间或者耐心,杰夫 · 阿特伍德 把底线编得很漂亮:

enter image description here

最终,C + + 比 最初,它慢了13倍。快了2倍

作为 Rico 总结一下:

所以我为我的惨败感到羞耻吗? 不,托管代码 几乎不费吹灰之力就取得了非常好的成绩 管理版本,雷蒙德必须:

  • 写他自己的文件夹

  • 编写自己的字符串类

  • 自己编写分配器

  • 自己写国际地图

当然,他使用了可用的底层库来实现这一点, 但还有很多工作要做,你能把剩下的叫做 STL 吗 程序? 我不这么认为。

这是我11年来的经验,谁知道以后还有多少个 C #/C + + 版本。

当然,这并非巧合,因为这两种语言完成了截然不同的设计目标。C # 希望用在以开发成本为主要考虑因素的地方(仍然是大多数软件) ,而 C + + 的亮点在于,你可以毫不费力地从你的机器中榨取每一分性能: 游戏、算法交易、数据中心等等。

不要让混乱!

  • 如果 C # 应用程序是在最佳情况下编写的,而 C + + 应用程序是在最佳情况下编写的,那么 C + + 会更快。
    这里有很多关于为什么 C + + 比 C # 更快的原因,例如 C # 使用类似于 Java 中的 JVM 的虚拟机。基本上较高级别的语言性能较差(如果在最佳情况下使用)。

  • 如果您是一位经验丰富的专业 C # 程序员,就像您是一位经验丰富的专业 C + + 程序员一样,那么使用 C # 开发应用程序要比使用 C + + 容易和快得多。

在这些情况之间可能存在许多其他情况。例如,您可以编写一个 C # 应用程序和 C + + 应用程序,以便 C # 应用程序比 C + + 1运行得更快。

在选择语言时,你应该注意项目的环境和主题。对于一个通用的商业项目,你应该使用 C # 。对于一个需要高性能的项目,如视频转换器或图像处理项目,你应该选择 C + + 。

更新:

好的。让我们比较一下为什么 C + + 最可能的速度比 C # 快的一些实际原因。考虑一个编写良好的 C # 应用程序和相同的 C + + 版本:

  • C # 使用 VM 作为执行应用程序的中间层。
  • AFAIK CLR 无法优化目标机器中的所有 C # 代码。C + + 应用程序可以在目标机器上编译,具有最大的优化。
  • 在 C # 中,对运行时最可能的优化意味着最可能的快速 VM。
  • C # 是一种更高级的语言,因此它为最终的进程生成更多的程序代码行。(考虑 Assembly 应用程序和 Ruby 应用程序之间的区别!相同的条件是在 C + + 和更高级的语言(比如 C #/Java)之间

如果你更喜欢作为一个专家在实践中获得更多的信息,看这个。它与 Java 有关,但也适用于 C # 。

一个更好的方法来看待它,一切都比 C/C + + 慢,因为它抽象了,而不是遵循棍棒和泥浆范例。这就是所谓的系统编程的原因,你的程序对粮食或裸金属。这样做还可以获得其他语言(如 C # 或 Java)无法达到的速度。但可惜的是,C 语言的根本原因在于做事情的方式比较困难,所以大多数情况下,你需要编写更多的代码,并花费更多的时间来调试它。

C 也是区分大小写的,C + + 中的对象也遵循严格的规则集。例如,一个紫色的冰淇淋蛋卷筒可能不同于一个蓝色的冰淇淋蛋卷筒,虽然它们可能是蛋卷筒,但它们可能不一定属于蛋卷筒家族,如果你忘了定义什么是蛋卷筒,你就会被挤出去。因此,冰淇淋的特性可能是克隆的,也可能不是克隆的。关于速度,C/C + + 使用堆栈和堆的方法,这就是裸金属得到金属的地方。

随着提高库,你可以实现难以置信的速度不幸的是,大多数游戏工作室坚持标准库。另一个原因可能是因为用 C/C + + 编写的软件在文件大小上往往很庞大,因为它是一个巨大的文件集合而不是一个单独的文件。还要注意的是,所有的操作系统都是用 C 编写的,所以一般来说,为什么我们必须问这个问题,什么可以更快? !

另外,缓存也不比纯内存管理快,抱歉,这只是没有扇形。内存是物理的东西,缓存是软件为了提高性能而做的事情。人们还可以推断,如果没有物理内存缓存,就根本不会存在。无论是自动的还是手动的,都必须在某个级别上管理内存,这并不排除这个事实。

主要关注的不是速度,而是 Windows 版本和升级的稳定性。Win32基本上不受 Windows 版本的影响,因此非常稳定。

当服务器退役和软件迁移时,围绕任何使用。网络和通常很多的恐慌。但是一个10年前构建的 Win32应用程序就像什么都没发生过一样一直运行。

我专门从事优化工作已经有15年了,经常重写 C + + 代码,尽可能多地使用编译器内部特性,因为 C + + 的性能通常远不及 CPU 的能力。通常需要考虑缓存性能。为了替代标准的 C + + 浮点数代码,需要许多矢量数学指令。 大量的 STL 代码需要重新编写,并且运行速度往往要快很多倍。当 CPU 接近其最佳性能时,大量使用数据的数学和代码可以得到惊人的重写结果。

这些在 C # 中都是不可能的。比较他们相对的实时表现真的是一个无知到令人难以置信的问题。C + + 中最快的一段代码是当每一条汇编指令都为手头的任务进行优化时,完全没有不必要的指令。在需要的时候使用每段内存,而不是复制 n 次,因为这是语言设计所要求的。每个所需的内存移动都与缓存协调工作。 在最终的算法不能改进的地方,基于准确的实时要求,考虑到精度和功能。

那么你将接近一个最佳解决方案。

将 C # 与这种理想情况进行比较是令人震惊的。C # 竞争不过。事实上,我目前正在重写一大堆 C # 代码(当我说重写的时候,我的意思是完全删除和替换它) ,因为它们甚至不在同一个城市,更不用说提高实时性能了。

所以拜托,别再自欺欺人了。C # 很慢。非常慢。所有的软件都在减速,而 C # 使得这种速度下降更加严重。所有的软件都使用汇编程序中的提取执行周期(你知道的,在 CPU 上)运行。你使用了10倍的指令,它的速度会慢10倍。你破坏了缓存,它会运行得更慢。你将垃圾收集添加到一个实时的软件,然后你经常被愚弄,以为代码运行“ OK”,只是有那么一些时刻,然后当代码去“有点慢了一段时间”。

Try adding a garbage collection system to code where every cycle counts. I wonder if the stock market trading software has garbage collection (you know – on the system running on the new undersea cable which cost $300 million?). Can we spare 300 milliseconds every 2 seconds? What about flight control software on the space shuttle – is GC ok there? How about engine management software in performance vehicles? (Where victory in a season can be worth millions).

实时垃圾收集完全失败。

所以,不,强调一下,C + + 要快得多,C # 是一个倒退。