C#性能问题

11

我刚刚参加了一次IKM C#测试。其中一个问题是:

以下哪个能够提高C#程序的性能?

  • A. 使用装箱
  • B. 使用拆箱
  • C. 不使用常量
  • D. 使用空析构函数
  • E. 使用值类型而不是引用类型

最终我跳过了这个问题,我唯一能看到的可能答案是E。在某些情况下,值类型可以提供更好的性能(对于小类型:不需要解引用且不在托管堆上[假设不是引用类型的成员]),但并非总是如此。


2
你能在这里发布其他问题吗? - Cosmin
2个回答

19

关于错误答案的一些说明:

装箱转换是将值类型值转换为引用类型值,即对象。它涉及从垃圾回收堆中分配内存,创建一个对象头,该对象头将对象标识为值类型所属的类型,并将值类型值位复制到对象中。这是创建类型系统幻觉的转换,使得值类型派生自System.ValueType和System.Object。在.NET 1.x程序中广泛使用装箱转换,因为它仅支持System.Collections中的类作为集合类型,这些集合类型的元素是Object。.NET Generics在2.0中添加使得这些类立即过时,因为它允许创建System. Collections.Generic中的类,可以存储值而不必进行装箱。所以不对。

拆箱是相反的转换,即从装箱对象值返回值类型值。与装箱不太昂贵相比,它仅涉及检查装箱对象的类型是否为预期类型并复制值类型值位。它需要在C#中进行强制转换,如果装箱值类型不匹配,则容易引发异常。与之前的一样,也不对。

带有const关键字的标识符是直接编译到编译器生成的IL中的文字值。另一种是readonly关键字,它需要访问内存以加载值,因此始终较慢。const标识符应始终为private或internal,公共常量容易在部署修复程序时破坏程序,因为您更改值但未重新编译使用常量的程序集。这些程序集仍将使用旧的常量值,因为它已编译到其代码中。这是readonly值不会出现的问题,所以也不对。

析构函数(也称为终结器)会大幅增加对象的成本。垃圾回收器确保在对象被垃圾回收时调用终结器。但为此,它必须单独跟踪对象,这种对象被放置在终结器队列上,等待终结器线程执行终结器。该对象实际上直到下一次 GC 才真正被销毁。你几乎总是需要为这样的对象实现 IDisposable 类,这样程序可以早期调用终结器的职责,而不是让运行时自动完成。在 Dispose() 方法中调用 GC.SuppressFinalize()。最糟糕的情况就是一个没有任何作用的终结器,所以不要留下它。

.NET 中存在值类型的原因是它们比引用类型更高效。它们的值占用比引用类型对象少得多的内存,并且可以存储在 CPU 寄存器和 CPU 堆栈中,这些内存位置在处理器设计中高度优化。他们对语言设计构成了负担,因为将它们抽象为对象是一种漏洞的抽象,会吞噬无法检测到的 CPU 周期,特别是当你尝试变异结构体时,这是一种困难的类型,容易破坏程序。但为了避免像 Smalltalk 这样的超纯语言所遭受的性能损失,使用值类型是很重要的。Smalltalk 是一种先锋 OOP 语言,在其中每个值都是一个对象,并影响了大量后续的 OOP 语言。但由于其性能差且没有明确的路径让硬件工程师将其快速地运行,它很少被实际使用。与 C# 不同,C# 不会抽象处理器设计,因此使用 C# 可以避免这种问题。


1
Smalltalk其实在各个行业都被成功地广泛应用,从华尔街投资银行到集装箱运输再到硅晶圆生产。IBM曾经重新培训过成千上万的Smalltalk顾问。事实上,Smalltalk的实现比Java语言的实现更快,但Java被免费使用赠送了。然而,由于其性能不佳,没有清晰的路径供硬件工程师将其尽可能快地抽象化处理,Smalltalk并未得到广泛使用。 - igouy
当然,30年的硬件改进完全改变了平衡:Smalltalk在过去十年中已经足够快,并且仍然是一个更具生产力的环境。 - Stephan Eggermont

8
答案最可能是E。在几乎所有情况下,值类型将提高性能。首先,在函数中使用值类型会创建堆栈空间,该空间甚至在调用之前分配,避免了对象分配开销。其次,当在堆上创建值类型数组时,您不仅避免了对象分配开销,而且数据往往更加缓存一致。
确实,复制值类型可能存在潜在的内存带宽开销,但现代内存带宽非常巨大,通常其损失被其他节省严重压倒。此外,处理64位或更小类型时,实际上没有损失。

我知道值类型可以提高性能,但问题的措辞表明,只有在某些情况下引用类型才能提供更好的性能,你才会选择E作为答案。不用担心,只是确保我没有漏掉任何明显的东西。 - MrNick
3
拳击、开箱装箱(boxing, unboxing)、无常量和空析构函数可能会导致性能变差。MSDN 明确警告空析构函数的性能成本: Destructors -“不应该使用空析构函数。当一个类包含一个析构函数时,会在 Finalize 队列中创建一个条目。当调用析构函数时,垃圾回收器会被调用来处理队列。如果析构函数为空,则只会导致不必要的性能损失。” - Brian
因此,我说答案“可能”是E。正如Brian指出的那样,其他选项没有任何可以提高性能的方面 - 只有E有潜力这样做,因此它是最有可能的答案。 - Kevin Hsu

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