CLR泛型和代码共享
First up – 泛型的高级位和代码共享
我们基于变量的类型,做了代码共享和匹配的混合。
对于引用类型的变量,泛型方法实例化了代码共享。
对于内置类型和值类型,包括枚举,泛型方法是专业化的。
什么是代码共享?
就泛型而言,代码共享是指有两个或多个“兼容”方法的实例指向了同一段x86代码。例如Foo.M<MyClass1>和Foo.M(MyClass2)共享同样的x86代码,MyClass1和MyClass2是引用类型。
简单的历史 – 我们在v1.0和v1.1里,同样对数组类型的引用类型做了代码共享。
快速回顾EE(执行引擎)数据结构
关于CLR的执行引擎数据结构,在SSCLI Essentials书里有很好的解释。概述如下:
在堆上所有的对象,都有一个固定大小的指针指向方法表,方法表描述了对象类型标识符(你实际上通过RuntimeTypeHandle得到了托管代码的表现形式)。方法表不仅包含了指向EE结构的指针,更重要的是,类型方法的列表和他们 表现得代码指针。这些指针可以指向x86代码,也可以指向JIT stub(可以调用JIT如果该方法还没有被JIT)。方法表广泛的用于类型定义。
MethodDesc是一个用于描述方法的小结构体。每个方法都有一个代表性的MehodDesc 结构体,尽管没有被运行时广泛的使用,除非你尝试使用迟绑定(反射)。在运行时里MehodDesc有不同的类型,但是对于发送(post)而言,我们可以假设他们都一样。
调用一个实例的方法在概念上就像这样:从自身的指针开始,指向方法表(方法指针代码的索引),然后调用代码指针,把自己的地址作为变量传递,这是每个x86调用的惯例。调用静态方法实际上是做同样的事情,只是没有“this”指针作为变量传递。当然了,JIT可以生成代码直接调用各种指针- 在JIT时,他做了一个很大的方法的表用于查找代码指针。
中间语言的泛型方法
当我们在描述泛型的时候,我们有一个未知的问题-当我们的JIT编译时,我们对局部变量 T 做了什么处理? 怎样生成x86的代码?
让我们先考虑下如下的代码段:
class Foo
{
[MethodImpl(MethodImplOptions.NoInlining)]
public void M1<T>()
{
Console.WriteLine(typeof(T));
}
}
Foo f1 = new Foo().M1<string>();
Foo f2 = new Foo().M1<object>();
当我们不知道T是什么类型的时候,M1<T>在代码中的表现形式是什么?我们实际上指定了类型参数“!!arity”,arity是泛型类型参数的索引(0,1…)
Foo.M1<T>的中间语言如下所示:
// IL Code for Foo.M1<T>
.method public hidebysig instance void M1<([mscorlib]System.Object) T>() cil managed noinlining
{
// Code size 17 (0x11)
.maxstack 8
IL_0000: ldtoken !!0
IL_0005: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_000a: call void [mscorlib]System.Console::WriteLine(object)
IL_000f: nop
IL_0010: ret
} // end of method Foo::M1
我们不用对类型做任何假设,该方法将会被传递。你可以想象,在抽象的机器世界里,调用这个方法,我们将基于传递的泛型变量,用字符串或者对象将!!0替换掉。这个场景与JIT编译时是很相近的。
进入泛型方法与代码共享:
查看代码,我有Foo类型的两个实例,调用本质上不同的方法(例如:M1<object> 与M1<string>是不一样的,同样也是通过反射来表现)。通过代码共享,这两个方法的实例是指向同一段x86的代码。
正如这样:当我们遇到这段代码: Foo f1 = new Foo().M1<string>(), JIT第一次对这个方法进行编译,我们得到如上的中间编译语言,使用一个指针替代!!0去调用运行时,以获得该类型的类型信息,将其传递到泛型变量slot()。这个指针我们是从什么地方获得的了?好吧,这来自于一个奇怪的调用惯例:我们为这个调用提供了一个隐藏的变量,是一个指向了运行数据结构体的指针为我们提供了信息。
带来的隐藏变量的数据结构
一个指向什么地方的指针?好吧,在一个指定的实例里,我们只要知道提供给泛型方法的类型参数, 我们创造并且传递给MehodDesc指针用于方法的特殊声明。对于Foo.M1<string> 我们传递到MethodDesc的事M1<string>,对于Foo().M1<object>,我们传递到MethodDesc的是M1<object>。MethodDesc实际上包含了用于确定!!0的所有信息。JIT编译的x86代码将会从MethodDesc指针里得到类型信息。
所以,对于上面的例子而言,JIT 遇到“Idtoken !!0”,编译的x86代码从隐藏变量里获得传进去的变量指针。对于string和object都是一样工作的,因为我们是间接的获得指针,而不是指定的string和object。
我们在什么时候需要“隐藏”变量
我们需要生成隐藏变量用于一些例子,这些例子不能从可用的x86执行时获得类型信息。如下是一些例子,伴随着隐藏的数据结构体:
1. Foo<T> static M() == TypeHandle
2. Foo<T> static M<T> == MethodDesc
3. Foo M<T> == MethodDesc
4. Foo static M<T> == MethodDesc
第四种类型是最有趣的,我们实际上需要TypeHandle和MethodDesc, 但是JIT知道我们可以从MehodDesc获得TypeHandle生成代码(这是一种间接的情况,正如我在SSCLI描述的一样)。
没有指针的一种情况是Foo<T> M(),因为M是一个实例化方法,并且我们已经按照惯例传递了“this”指针,我们可以从”this”获得变量T里获得变量类型。
对于这些问题,为什么传递MethodDesc/TypeHandle,为什么我们不仅仅将类型作为隐藏变量传递。很好的问题,对于有不知一个泛型参数的情况,这有效的减少了参数传递的调用时间。例如M<U,W,Z>,MethodDesc已经很好的描述了他,所以这样更容易,更有效的将他传递到MD指针,并且合适的生成index代码的索引。
我们为什么对内置类型和值类型缺少代码共享?
从技术层面上讲,我们可以对内置类型和值类型进行代码共享,但是很明显这样效率很低。为了让他变得更容易理解:
考虑:
Foo{M<T>() {}}
new Foo().M<int>();
new Foo().M<double>();
JIT实际上会生成两个分离的代码段用于实例化,为什么?好吧,内置类型和值类型是在栈上的,不需要方法表的引用(除非它们被封装了)。通常,不同的值类型/内置类型有不同的数据大小,整形和双精度型就是个很好的例子。JIT对于未知的数据尺寸,不能生成x86的代码,所以将他们特殊化。
运行时可以通过封装值类型/内置类型来实现共享,但这是否阻止了对泛型不必要的封装?
部分特定化
我不是很确定如果我们的CLR团队是否调用了部分特定化,但是我将调用它用于博客的发送。对于具有值类型和引用类型的例子,我们对值类型特殊化,并且链接引用类型共享的代码。
Foo<int>.M<string> 和Foo<int>.M<object> 是相同的代码. 我们共享了Foo<int> 里的M<T>。
引用:http://blogs.msdn.com/b/joelpob/archive/2004/11/17/259224.aspx