C#比C ++慢吗?这是一个很大的问题。作为初级开发人员,我确信答案是“肯定的”。现在,我经验丰富了,我知道这个问题并不明显,甚至很复杂。
在尝试回答这个问题之前,还有另一个问题:“真的重要吗?”。对于现代CPU,开发效率应该比性能重要得多,对吗?而且用C#编写的代码比用C ++编写的代码生产力要高得多,所以这并不意味着我们要切换到C ++只是因为它快一点。
我认为这确实很重要。详细了解答案将有助于改进两种语言。您可能会学习如何编写性能更高的C#代码。或者,我们可能会改进C ++语言及其库以提高生产力。或者,您可以将对性能敏感的热路径移至C ++,通过互操作从C#调用此代码。另外,如果两种语言都具有相同的性能,则可以首先避免很多不必要的工作切换到C ++。除此之外,回答这是一个非常有趣的练习。
托管代码与本机代码
比较C#和C ++会导致一个更普遍的问题:“托管代码是否比本地代码慢?”。两者之间到底有什么区别?有几个区别。其中之一是将本机代码(例如C或C ++)直接编译为机器代码,可以由计算机执行。另一方面,托管代码首先被编译为“中间”代码。那是Java中的字节码和C#中的中间语言(IL)代码。然后,在运行时,即时(JIT)编译器将该代码编译为机器代码。这样做的原因是同一代码可以在不同的机器上以不同的方式编译。因此,例如,可以将相同的IL代码编译到Windows和Linux。
另一个很大的不同是内存管理,但让我们先从IL代码的含义开始。
IL代码与机器代码
在托管代码中,运行时还有另一个编译阶段。IL代码编译为机器代码。难道这意味着托管代码总是比本地代码慢吗?好吧,默认情况下,是的。首次在C#中调用方法时,它将JIT编译为机器代码。这需要时间。但是,每种方法只发生一次。这就是.NET进程的启动时间将比C ++进程的启动时间更长的原因。好吧,至少是原因之一。
但是,启动时间并不是一个大问题,原因如下:在服务器中,启动时间并没有真正起作用。服务器运行时间很长,可以忽略不计。即使一天要部署几次,也可以部署到暂存环境,等待启动完成,然后在DNS级别“交换”暂存和生产。
对于桌面应用程序和移动设备,启动时间确实是一个很大的问题。但是,这可以通过一种称为“时间提前(AOT)编译”的技术来解决。在这里,您可以在运行之前将IL代码编译为机器代码。在C#中,这是通过称为Ngen.exe的工具完成的,该工具应在用户的计算机上执行。通常,这是安装程序中的另一步骤。对于移动设备,JIT编译不是一个选择。有一些设备限制不允许这样做,因此AOT编译是必需的。这在Mono运行时中已经完成,并通过一项称为ReadyToRun Images(R2R)的技术添加到.NET Core中。
因此,如果JIT编译不是问题,那么C#代码和C ++代码应该以相同的速度运行,对吗?毕竟,这是相同的机器代码指令。JIT编译问题只是其中的一部分。您认为哪种代码会产生更好的机器代码?将C ++代码直接编译成机器代码?还是将C#代码编译为IL代码然后再转换为机器代码?哪个将更优化且指令更少?
假设C#编译器和C ++编译器都进行了出色的优化。实际上,我认为这非常接近事实。现在考虑C#代码实践。我们经常使用诸如LINQ,异步/等待状态机,模式匹配和反射之类的东西。这些都是有用的和富有成效的功能,但也很慢。在C ++中,我们通常将代码编写得更接近于机器代码。我们直接使用内存指针,并且几乎没有高级抽象。因此,即使可以用C#编写低级代码,也可以用C ++编写高级代码(现在有很多库,包括C ++ LINQ),大多数C ++代码将以更少的指令生成更快的机器代码。
总之,从理论上讲,您可以创建与C ++代码一样快的C#代码。但是,在大多数情况下,由于编码习惯,C ++代码将变得更快。差异通常并不重要,但在热路径和算法中确实很重要。因此,作为C#开发人员,确保优化性能敏感的代码。
内存管理
托管代码的另一个方面是内存管理。在C#等托管语言中,公共语言运行时(CLR)对内存完全负责。它将为每个分配查找内存空间,并在不再引用时自动删除内存。这称为垃圾收集。垃圾收集器还会移动内存以防止碎片问题。那是您多次分配和删除内存的时间,直到内存缓冲区中出现“空洞”。这使分配新内存的速度变慢,即使有足够的可用内存,最终也可能导致内存不足崩溃。在C#中,垃圾回收器不断地将内存缓冲区彼此相邻移动。这样,它确切地知道了在哪里分配内存,不需要寻找空闲的“空洞”(尽管大对象堆仍然存在碎片问题)
在本地语言中,分配和释放是由程序员控制的。一条new语句直接在堆上分配代码,并delete释放内存。由于内存从未像.NET那样“移动”,因此效果很好。除了它为人为错误创造了很大的空间。如果无法删除内存,它将被永久占用并导致内存泄漏。如果有足够的内存泄漏,您将开始遇到性能问题,并最终崩溃。但是我们在这里是在理论上进行讨论,因此让我们假设所有C ++程序员都创建了完美的代码,而这些代码永远不会发生内存泄漏。
那么C ++代码或C#代码哪个更快?好吧,垃圾回收虽然很棒,但是却需要时间。在垃圾回收期间,线程通常必须暂停执行。因此,在您的流程中运行的代码会执行垃圾回收逻辑,而不是执行用户代码。话虽如此,垃圾收集器已进行了优化。在最新版本的GC中,它甚至可以在另一个线程中在后台运行,而丝毫不影响性能(有时)。
因此,要问的问题是,垃圾回收开销是否会比碎片问题开销更多地损害性能?在短期内,当应用程序刚刚开始运行时,碎片化就不是问题,垃圾回收肯定是问题。因此,C ++在程序启动时肯定更快。从长远来看,当您的应用程序连续运行数小时和数天时,碎片问题将迎头赶上。分配将变慢,并且在某些情况下,它将导致崩溃。
C ++开发人员知道此问题并有解决方法。一种解决方案是自定义分配器,它可以重用内存缓冲区或巧妙地分配以最大程度地减少碎片问题。
另一个解决方案是尽可能多的在堆栈上分配对象而不是堆。堆栈存储器从定义上看没有碎片问题,并且还有其他一些好处。在堆栈上使用对象比在堆上快得多。无需读取内存引用,然后去实际对象的那个位置。由于CPU高速缓存,堆栈存储器的工作效果更好。另一个好处是您不必手动删除对象,因此没有人为错误的地方。
在C ++中,分配堆栈非常有效,但在C#中则不是。在C ++中,您可以在堆栈上分配任何对象,无论是类还是结构。您可以轻松地通过引用传递堆栈分配的对象。在C#中,类型定义是在堆还是在堆栈上分配。像struct这样的值类型分配在堆栈上,而像类这样的引用类型则分配在堆上。像数组这样的集合List在堆上分配。尽管从C#7.3开始,您可以使用new stackalloc关键字在堆栈上分配数组并通过引用传递它们。就性能而言,“堆栈与堆”问题是如此重要,以至于最新的C#版本已投入大量资金来允许更多的堆栈分配并通过引用传递这些对象。尽管如此,C ++的指针仍然更适合于此。
因此,C ++在堆栈分配方面更好,并且没有垃圾回收开销。另一方面,C#没有碎片问题。除了生产力问题之外,我认为C ++在这方面的整体速度更快。主要是由于堆栈分配,而不是因为缺少垃圾收集器。C#在新功能stackalloc和Span功能方面正在迎头赶上。
摘要
我在这篇文章中的思考过程可能不会描述C#和C ++性能的所有方面。我敢肯定双方都有更多论点(我希望您能发表评论并分享这些观点)。而且,在性能方面,任何论点都只是猜测,没有基准来支持它。在这种特定情况下,创建正确的基准非常困难,因为要比较的场景和事物太多了。
我的结论仍然是,默认情况下,C#在大多数情况下不如C ++快。但是我认为它并不会慢很多,通常也没关系。当您确实具有对性能敏感的代码时,可以优化C#并获得与C ++几乎相似的性能。这可以通过堆栈分配来完成,从而避免了LINQ,重复使用内存和许多其他性能优化。