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

原文地址:https://www.cnblogs.com/longcloud/p/3134439.html