如何实现虚拟generics方法调用?

我对CLR如何实现这样的调用感兴趣:

abstract class A { public abstract void Foo(); } A a = ... a.Foo(); // <=== ? 

这个调用会导致某种类型的哈希映射查找类型参数令牌作为键和编译的generics方法专门化(一个用于所有引用类型,所有值类型的不同代码)作为值?

我没有找到关于这方面的确切信息,所以这个答案的大部分基于2001年的.Netgenerics的优秀论文 (甚至在.Net 1.0出来之前!), 后续文章中的一个简短说明以及什么我收集了SSCLI v.2.0源代码 (尽管我无法找到调用虚拟generics方法的确切代码)。

让我们开始简单:如何调用非generics非虚方法? 通过直接调用方法代码,使编译后的代码包含直接地址。 编译器从方法表中获取方法地址(参见下一段)。 可以这么简单吗? 好吧,差不多。 方法是JITed这一事实使它变得有点复杂:实际调用的是编译方法的代码,然后只执行它,如果它还没有编译; 或者它是一个直接调用已编译代码的指令(如果它已经存在)。 我将进一步忽略这个细节。

现在,如何调用非generics虚拟方法? 与C ++等语言中的多态性类似,可以从this指针(引用)访问方法表。 每个派生类都有自己的方法表及其方法。 因此,要调用虚方法,获取this的引用(作为参数传入),从那里获取对方法表的引用,查看其中的正确条目(条目号对于特定函数是常量)和调用入口指向的代码。 通过接口调用方法稍微复杂一点,但现在对我们来说并不感兴趣。

现在我们需要了解代码共享。 如果类型参数中的引用类型对应于任何其他引用类型,则代码可以在同一方法的两个“实例”之间共享,并且值类型完全相同。 因此,例如C.M()C.M() C.M()共享代码,但不与C.M()共享。 类型类型参数和方法类型参数之间没有区别。 (2001年的原始文章提到,当两个参数都是具有相同布局的struct时,代码也可以共享,但我不确定在实际实现中是否正确。)

让我们在通用方法的过程中采取中间步骤:generics类型中的非generics方法。 由于代码共享,我们需要从某处获取类型参数(例如,用于调用代码,如new T[] )。 出于这个原因,generics类型的每个实例化(例如CC )都有自己的类型句柄,它包含类型参数和方法表。 普通方法可以this引用访问此类型句柄(技术上是一个混淆称为MethodTable的结构,即使它包含的不仅仅是方法表)。 有两种类型的方法无法做到这一点:静态方法和值类型方法。 对于那些,类型句柄作为隐藏参数传入。

对于非虚拟generics方法,类型句柄是不够的,因此它们获得包含类型参数的不同隐藏参数MethodDesc 。 此外,编译器无法将实例存储在普通方法表中,因为它是静态的。 因此,它为generics方法创建了第二个不同的方法表,该表由类型参数索引,并从那里获取方法地址(如果已存在兼容类型参数)或创建新条目。

虚拟generics方法现在很简单:编译器不知道具体类型,因此它必须在运行时使用方法表。 并且不能使用普通方法表,因此必须在特殊方法表中查找generics方法。 当然,包含类型参数的隐藏参数仍然存在。

研究这个问题时学到了一个有趣的知识:因为JITer非常懒惰,以下(完全没用)代码可以工作:

 object Lift(int count) where T : new() { if (count == 0) return new T(); return Lift>(count - 1); } 

等效的C ++代码会导致编译器放弃堆栈溢出。

是。 特定类型的代码由CLR在运行时生成,并保留哈希表(或类似)的实现。

通过C#的CLR页面372:

当使用generics类型参数的方法是JIT编译时,CLR接受方法的IL,替换指定的类型参数,然后创建特定于在指定数据类型上操作的该方法的本机代码。 这正是您想要的,也是generics的主要特征之一。 但是,这有一个缺点:CLR不断为每个方法/类型组合生成本机代码。 这被称为代码爆炸。 这可能最终大大增加应用程序的工作集,从而损害性能。 幸运的是,CLR内置了一些优化function以减少代码爆炸。 首先,如果为特定类型参数调用方法,并且稍后使用相同的类型参数再次调用该方法,则CLR将仅编译此方法/类型组合的代码一次。 因此,如果一个程序集使用List,并且完全不同的程序集(在同一AppDomain中加载)也使用List,则CLR将仅为List编译一次方法。 这大大减少了代码爆炸。

编辑

我现在遇到了我现在遇到https://msdn.microsoft.com/en-us/library/sbh15dya.aspx ,它明确指出使用引用类型的generics重用相同的代码,因此我会接受它作为权威权威。

原始答案

我在这里看到两个不同意的答案,并且都提到了他们的一面,所以我会尝试加上我的两分钱。

首先,由Microsoft Press出版的由Jeffrey Richter发布的Clr via C#与msdn博客一样有效,特别是因为博客已经过时了(因为他的更多书籍请看http://www.amazon.com/Jeffrey- Richter / e / B000APH134必须同意他是windows和.net的专家。

现在让我做自己的分析。

显然,包含不同引用类型参数的两个generics类型不能共享相同的代码

例如,List 和List >不能共享相同的代码,因为这会导致通过reflection将TypeA的对象添加到List 的能力,并且clr在遗传学上也是强类型的, (与Java不同,只有编译器validationgenerics,但底层JVM对它们没有任何线索)。

这不仅适用于类型,也适用于方法,因为例如类型T的generics方法可以创建类型为T的对象(例如,没有什么能阻止它创建新的List ),在这种情况下重用相同的代码会造成严重破坏。

此外,GetType方法不可覆盖,实际上它总是返回正确的generics类型,certificate每个类型参数确实有自己的代码。 (这一点比看起来更重要,因为clr和jit基于为该对象创建的类型对象,通过使用GetType(),这意味着对于每个类型参数,即使对于引用类型,也必须有一个单独的对象)

代码重用会导致另一个问题,因为运算符将不再正常运行,并且通常所有类型的转换都会出现严重问题。

现在进行实际测试:

我通过使用包含静态成员的generics类型来测试它,而不是创建具有不同类型参数的两个对象,并且静态字段是不共享的,显然即使对于引用类型也不共享代码。

编辑:

有关如何实现的信息,请参见http://blogs.msdn.com/b/csharpfaq/archive/2004/03/12/how-do-c-generics-compare-to-c-templates.aspx :

空间使用

C ++和C#之间的空间使用是不同的。 因为C ++模板是在编译时完成的,所以在模板中每次使用不同类型都会导致编译器创建单独的代码块。

在C#世界中,它有些不同。 使用特定类型的实际实现是在运行时创建的。 当运行时创建类似List的类型时,JIT将查看是否已创建该类型。 如果有,它只是用户编码。 如果没有,它将采用编译器生成的IL并对实际类型进行适当的替换。

那不太正确。 每种值类型都有一个单独的本机代码路径,但由于引用类型都是引用大小的,因此它们可以共享它们的实现。

这意味着C#方法在磁盘和内存中的占用空间应该更小,因此这对于generics而言比C ++模板更具优势。

事实上,C ++链接器实现了一个称为“模板折叠”的function,其中链接器查找相同的本机代码段,如果找到它们,则将它们折叠在一起。 所以它看起来并不是那么明确。

正如人们可以看到CLR“可以”重用引用类型的实现,就像当前的c ++编译器一样,但是不能保证这一点,对于使用stackalloc和指针的不安全代码,可能不是这样,并且可能还有其他情况也是如此。

但是我们必须知道在CLR类型系统中,它们被视为不同的类型,例如对静态构造函数的不同调用,单独的静态字段,单独的类型对象,以及类型参数T1的对象不应该能够访问具有类型参数T2的另一个对象的私有字段(尽管对于相同类型的对象,确实可以从相同类型的另一个对象访问私有字段)。