C# 2.0泛型探讨

----- original

前些天尝试让CSDN的.NET论坛增加一点讨论的气氛,于是发了这个帖子:

C# 2.0会给我们带来什么

说来说去还是说到了泛型。但我觉得这个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次得到的结果。注意横向的比较(标为红色的部分)。

所以我的结论是:

  1. 泛型总是能带来性能提升,但提升不大;或者说仅在int这样很小的数据类型上才有很明显的作用
  2. 泛型并不能解决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依然适用。

posted on 2008-07-01 17:29  RIVERSPIRIT  阅读(353)  评论(1编辑  收藏  举报