The need for Constraints
C++将Generic Programming(泛型编程)的概念发挥得淋漓尽致。作为C++唯一的标准库,STL主要是应用Template,而不是面向对象的继承机制来实现的。很自然的,我们会把CLR和C++的Generic实现进行对比。Constraint是CLR在实现Generic时独有的概念,而且Constraint的使用也确实对CLR Generics在功能和性能方面有些负面影响。So, Why do we need constraints?
CLR和C++在模板实例化采用了不同的策略:C++主要是编译期间实例化模板,也就是说当编译器看到程序中引用了vector<int>的时候,它就会用int代换vector<typename T>中的T,生成一份vector<int>的代码到目标文件,这样做有几个问题:1) 代码冗余。如果在不同的.cpp文件中引用了vector<int>,那么每个编译产生的.obj都会包含vector<int>代码的一个拷贝。Link.exe必须能够正确处理这种特殊情况才能避免代码冗余。2) 由于实例化发生在编译期间,即使vector<int>仅在一个极少被调用的code path上被使用,它的实例化代码也会自始至终占据一块内存。3) 缺乏动态泛型支持(C++本身没有完整地Runtime的类型支持,并且不建议使用RTTI,所以这是和Generic一致的;但是如果向C#/Java这样本身支持Reflection的语言不支持Generics Reflection,就显得有些怪异了)。
CLR选择了在Runtime动态实例化模板。编译器可以在没有泛型参数的情况下编译生成List<T>的扩展MSIL指令;在运行时刻,程序代码中第一次引用List<int>的时候,CLR才动态的使用int类型实例化List<T>,并且生成实例化的代码(实际上只有新的ValueType才会引发模板实例化,ReferenceType共享一个实例化的模板代码),这就解决了上面提到的两个问题。由于CLR完全理解Generic语义,就可以在Reflection当中添加Generic支持,第三个问题也迎刃而解。
但CLR的实现方法也带来了新的问题:如果作为实例化参数的类不能满足Generic Type的要求(这些要求多种多样,可能是具有某个方法,某个操作符重载等等),实例化失败将会导致运行时错误,而不是像C++那样的编译连接错误。我们总是希望在编译期间检查出尽可能多的错误,但是显然不可能让C#编译器逐行检查Generic Type的MSIL指令来判断泛型参数是否符合要求(E.g. 如果Generic Type包含一条MSIL: call 0!.SomeMethod(),那么T必须定义SomeMethod)。折衷的方案是在Generic Type的MetaData里面显式的指明它对泛型参数的要求,这就是Constraint了。所以Constraint实际上是Generic Type对泛型参数要求的MetaData表示,其目的是帮助编译器尽早验证参数的合法性。
So, Constraint By Signature or By Type?
Constraint在实现可以选择两种不同的语义,一是By Signature,C++实际上隐含了这种语义。例如:类型参数T必须定义一个返回bool,接受一个T类型参数的CompareTo函数;或者T必须重载operator+,等等。这种方式的优点很明显--灵活性大,我们可以自由表述对泛型参数的各种要求;它的缺点是需要非常复杂的Constraint语法(C++不需要显式指出Constraint)。另一种方式是By Type,指出泛型参数必须从某个类继承,或者实现某个接口,还是上面的例子,我们可以说T必须实现IComparable<T>这个接口。这是C#/.NET采用的方式,语法上看非常简单,和普通的类继承/接口实现非常相似;但是表达能力就不如By Signature的方法,像operator overload这些不能用接口描述的语义就很难表达了。
Anders Hejlsberg在一篇专访当中谈到过C++和C#(实际上是CLR)在Generic实现上的异同,也分析了By Type Constraint的一些限制。
The Performance Implication of Constraint
采用这种By Type的Constraint的确对Generic的性能有一些负面影响。例如:
1) 由于Constraint必须采用interface,Value Type Object必须先Box,然后调用Constraint方法。如果采用By Signature,这个Box/Unbox操作应该可以避免。
2) 我们分析过interface method call比direct method call 或者virtual method call要慢,采用Interface based Constraint即使对reference type也会造成interface call overhead。
我在去年的MVP Summit上曾经问到过Constraint对Generics Performance的影响。我相信如果在JIT这个层次引入相应的Heuristic,上面的一些性能影响应该是可以被最小化的。当然,无论如何,采用Template + Constraint的List、HashTable始终要比使用Object的版本性能要优越些。
打印 | 张贴于 2004-04-13 10:40:00 | Tag:.NET
留言反馈
此外,ValueTypeObject的Box/Unbox问题,我觉得是不是不一定啊?我记得没错的话,这个Contraint好像只是不能够约束到Object,没有说不能够约束到ValueType吧?也许可以这样来提高性能吧?
I think now we are at the same square:
1) Constraint是必需的。
2) By Signature在更能和性能上要优于By Type。
3) 如果能克服上面我说的这一点,那么By Signature Constraint应该是更好的选择。:)