C# 2.0泛型探讨
----- original
前些天尝试让CSDN的.NET论坛增加一点讨论的气氛,于是发了这个帖子:
说来说去还是说到了泛型。但我觉得这个feature真的没什么,最起码没有想象中的那么有用,和C++模板实际上差别很大的,为什么说到C# 2.0就都要先说泛型。最后我这样回复:
.NET的泛型,除了能用编译器更好的进行类型检查之外,别的地方用处不大。
并且,性能提升并不明显,装箱/拆箱的过程,不要和类型转换相混淆。以装箱为例:
int i = 5;
object o = i;
这里实际上做了两步操作,一个是把数据从线程堆栈复制到GC堆上,另一步是类型转换。这两步形成了装箱的操作。
泛型只能避免第二步,得到一点点性能提升;而最影响效率的地方却是第一步:把数据从线程堆栈复制到GC堆上。
C#的泛型也远不如C++模板那样灵活,要做“模板元编程”之类的高级用法,在C#根本就不可能。各位有兴趣的话可以比较一下C#和C++在这方面的差别。
PS.我这段大概要让人失望了,但这是事实,C#泛型被讨论的实在太多了,而实际上它的用处不大。其好处主要在编译时更好的类型检查,减少人为错误的可能性。
我目前能看到的好处仅在于“编译时更好的类型检查”而已 — 当然,这个好处也非常重要,但别的真的就没什么了。特别是,很多网友把C#泛型当作解决boxing/unboxing性能问题的药方,实际上根本就不是这样,这种错误的观点会带来更大的性能问题。
误导来自于这种测试代码:
int i = 5;
![]()
ArrayList list1 = new ArrayList();
list1.Add(i);
i = (int)list1[0];
![]()
List<int> list2 = new List<int>();
list2.Add(i);
i = list2[0];
没错,后者的性能几乎要比前面的高100%。这让人产生了泛型可以解决boxing/unboxing性能问题的错觉,但实际上这是因为这里是小小的int,boxing/unboxing和类型转换的代价是差不多的。但是可以从反面证明我的观点:
假如泛型能解决boxing/unboxing性能问题,那么这两段代码的运行速度应该是大致相当的:
struct LargeDataStruct
{
decimal A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z;
}
class LargeDataClass
{
decimal A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z;
}
![]()
LargeDataStruct data1 = new LargeDataStruct();
List<LargeDataStruct> list1 = new List<LargeDataStruct>();
list1.Add(data1);
data1 = list1[0];
![]()
LargeDataClass data2 = new LargeDataClass();
List<LargeDataClass> list2 = new List<LargeDataClass>();
list1.Add(data2);
data2 = list2[0];
实际运行速度如何?完全是天壤之别,根据我的测试,前者所花费的时间是后者的100倍左右;测试可能不精确,但一两个数量级肯定是有的。
----- update
要完整地评估泛型在性能上的问题,可以做这样一个表来对比:
Int32 | LargeDataStruct | LargeDataClass | |
ArrayList | 87.7 | 2808.7 | 76.9 |
List Generic | 31.3 | 1903.1 | 70.5 |
Athlon64 2800+/Biostar NForce4/Apacer 512M DDR400 x 2
这是在我的机器上的测试,把数据装入集合再从集合获取数据,循环50w次得到的结果。注意横向的比较(标为红色的部分)。
所以我的结论是:
- 泛型总是能带来性能提升,但提升不大;或者说仅在int这样很小的数据类型上才有很明显的作用
- 泛型并不能解决boxing/unboxing的问题,大型的值类型,还是应该设计为引用类型
这回应该说清楚了吧
----- update on
原贴名为“C#范型的用处不是很大”,现在改为“C# 2.0泛型探讨”。
我原来确实认为C#泛型用处不会很大,因为我看了很多介绍泛型的文章,像这个,把C#泛型的好处主要归纳为这三点:类型安全、二进制代码重用、性能。更好的类型安全是显而易见的,但后面两个好处并不明显。
关于“二进制代码重用”,是建立在“不使用泛型就得为一个功能的实现写多个版本的代码”的假设上的,然而实际上用List Generic能完成的事情ArrayList也能做到,只是类型安全很难保证。这样最终还是类型安全的好处。
关于泛型的性能,你可以看到这个帖子引来了这么多的讨论,我现在觉得这个 问题得分应用场合而讨论。
对于用泛型实现的集合类,比如System.Collections.Generic namespace下面那些,范型可以提升一些性能,但仍然无法避免boxing/unboxing带来的性能问题,所以依然不可滥用值类型。
— 这是我认为泛型在性能上也无太大优势的主要原因。最后我总结道:我写这个blog并不是说范型没用,而是说范型可能没有很多人想象中的那么有用。特别是,不要认为范型能解决boxing/unboxing带来的性能问题。就这样。
而对于其它一些领域的范型应用,比如我在回复中提到的quick sort算法,泛型确实可以提高很多的性能,我的测试代码显示它提高了三倍的性能。
— 这些领域的泛型应用大概是Ninputer反驳我的主要原因。他这样总结道:使用“object来支持任意类型”的做法相比,能够消除unboxing/boxing,因此在使用object后unboxing/boxing在你的程序里影响了性能的时候,泛型能够帮助你避免它。
这两种应用场合完全不同,对范型性能问题的研究自然有不同的结果。所以在各自的应用场合下,我和Ninputer的结论都应该是正确的。(这大概反映了每个程序员的关注点都很不一样)
我首先关注的是最常用和最重要的部分,对于.NET泛型,给程序员带来最大好处的肯定非System.Collections.Generic莫属。结果却是忽略了其他的一些方面。
----- update
泛型解决了boxing/unboxing的问题,但System.Collections.Generic下面的集合类没有解决性能问题(这个性能问题不是boxing/unboxing带来的,是数据复制带来的)。这应该是更精确的描述(换句话说,前面我说的是不对的)。
所以不要把大量的值类型大对象放到集合里,这个.NET 1.x里面的适用的结论在有了泛型的.NET 2.0依然适用。