我刚刚参加了 IKM C# 测试。其中一个问题是:
以下哪一项提高了 C# 程序的性能?
- A. 使用拳击
- B. 使用拆箱
- C. 不要使用常量
- D. 使用空析构函数
- E. 使用值类型而不是引用类型
最后我跳过了这个问题,我能看到的唯一可能的答案是 E。在某些情况下,值类型可以提供更好的性能(对于小类型:不需要取消引用,并且不在托管堆上[假设不是引用类型的成员]),但情况肯定并非总是如此。
我刚刚参加了 IKM C# 测试。其中一个问题是:
以下哪一项提高了 C# 程序的性能?
最后我跳过了这个问题,我能看到的唯一可能的答案是 E。在某些情况下,值类型可以提供更好的性能(对于小类型:不需要取消引用,并且不在托管堆上[假设不是引用类型的成员]),但情况肯定并非总是如此。
稍微关注一下错误的答案:
当值类型值转换为引用类型值(对象)时,就会发生装箱转换。它涉及从垃圾收集堆分配内存,创建一个对象标头,将对象标识为值类型的类型,并将值类型值位复制到对象中。这是创建类型系统错觉的转换,即值类型派生自 System.ValueType 和 System.Object。装箱转换在 .NET 1.x 程序中大量使用,因为它支持的唯一集合类型是 System.Collections 中的类,即元素为 Object 的集合。2.0 中添加的 .NET 泛型使这些类立即过时,因为它允许在 System.Collections.Generic 中创建类。它可以存储一个值而不必装箱。所以不行。
取消装箱是相反的转换,从装箱的对象值返回到值类型值。不像装箱那么昂贵,它只涉及检查装箱对象的类型是否为预期类型并复制值类型值位。它需要在 C# 中进行强制转换,并且在装箱值类型不匹配时容易引发异常。和上一个一样没有。
用const关键字标记的标识符是直接编译到编译器生成的 IL 中的文字值。另一种方法是readonly关键字。这需要访问内存来加载值,因此总是较慢。const标识符应该始终是私有的或内部的,当您部署更改值但不重新编译使用该常量的程序集的错误修复时,公共常量具有破坏程序的诀窍。这些程序集仍将使用旧的常量值,因为它已编译到它们的代码中。只读值不会发生的问题。所以不行。
析构函数(又名终结器)大大增加了对象的成本。垃圾收集器确保在对对象进行垃圾收集时调用终结器。但要做到这一点,它必须单独跟踪对象,这样一个对象被放在终结器队列中,等待终结器线程来执行终结器。在下一次GC 传递之前,该对象实际上并没有真正被销毁。您几乎总是让此类对象的类实现 IDisposable,因此程序可以及早调用终结器的职责,而不会因自动执行而给运行时带来负担。您在 Dispose() 方法中调用 GC.SuppressFinalize()。没有什么比不做任何事情的终结器更糟糕的了,所以不。
.NET 中存在值类型的原因很明确,即它们比引用类型更有效。它们的值比引用类型对象占用的内存要少得多,并且可以存储在 CPU 寄存器和 CPU 堆栈中,这些内存位置在处理器设计中得到了高度优化。它们给语言设计带来了负担,因为将它们抽象为对象是一种泄漏的抽象,它会不知不觉地吞噬 cpu 周期,特别是结构是一种困难的类型,当你试图改变它们时,它具有破坏程序的诀窍。但重要的是要避免像 Smalltalk 这样的超纯语言遭受的那种性能打击。一种开创性的 OOP 语言,其中每个值都是一个对象,并影响了大量后续的 OOP 语言。但实际上很少在任何地方使用,因为它的性能很差,没有明确的路径让硬件工程师让它与不抽象处理器设计的语言一样快。就像 C#。所以这使它成为E。
答案很可能是 E。几乎在所有情况下,值类型都会提高性能。首先,在函数中使用值类型会创建堆栈空间,该空间甚至在调用之前就已分配,从而避免了对象分配开销。其次,在堆上创建值类型的数组时,您都避免了对象分配开销,并且数据往往更加缓存一致。
确实,在值类型的复制中存在潜在的内存带宽开销,但现代内存带宽是如此巨大,损失通常被其他节省严重压倒。此外,在处理 64 位或更小的类型时,实际上没有损失。