如何实现虚拟泛型方法调用?

时间:2011-07-04 15:41:33

标签: c# .net clr

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

abstract class A {
    public abstract void Foo<T, U, V>();
}

A a = ...
a.Foo<int, string, decimal>(); // <=== ?

此调用是否会导致某种类型的哈希映射查找类型参数标记作为键和已编译的泛型方法特化(一个用于所有引用类型,一个用于所有值类型)作为值?

3 个答案:

答案 0 :(得分:13)

我没有找到关于此的更多确切信息,所以这个答案很大程度上基于excellent paper on .Net generics from 2001(甚至在.Net 1.0出来之前!),a follow-up paper中的一个简短说明以及我从SSCLI v. 2.0 source code收集(即使我无法找到调用虚拟泛型方法的确切代码)。

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

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

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

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

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

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

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

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

    return Lift<List<T>>(count - 1);
}

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

答案 1 :(得分:4)

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

CLR via C#的第372页:

  

当使用泛型类型的方法时   参数是JIT编译的CLR   采用方法的IL,代替   指定类型参数,然后   创建特定的本机代码   对该方法进行操作   指定的数据类型。这是完全正确的   你想要什么,是主要的   泛型的特征。但是,那里   这是一个缺点:CLR保持   为每个人生成本机代码   方法/类型组合。这是   称为代码爆炸。这个   最终会增加   应用程序的工作集   大幅度,从而伤害   性能。   幸运的是,CLR有一些   内置于其中的优化以减少   代码爆炸。首先,如果方法是   要求特定类型的参数,   然后,再次调用该方法   使用相同类型的参数, CLR   将为此编译代码   方法/类型组合只需一次。所以   如果一个程序集使用List,   和一个完全不同的组件   (也在同一个AppDomain中加载)   使用List,CLR会   编译List的方法   就一次。这减少了代码爆炸   基本上

答案 2 :(得分:-1)

修改

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

原始回答

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

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

现在让我自己做分析。

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

例如,List&lt; TypeA&gt;和列表&lt; TypeB&gt;&gt;不能共享相同的代码,因为这会导致能够将TypeA的对象添加到List&lt; TypeB&gt;通过反射,clr也在遗传学上强类型化(与Java不同,只有编译器验证泛型,但底层JVM对它们没有任何线索)。

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

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

代码重用会导致另一个问题,因为is和as将不再正常工作,并且通常所有类型的转换都会出现严重问题。

现在进行实际测试:

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

修改

请参阅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#方法的占用空间应该更小   磁盘和内存,这对于泛型而不是C ++是一个优势   模板。

     

实际上,C ++链接器实现了一个称为“模板”的功能   fold“,链接器查找本机代码段   相同,如果找到它们,将它们折叠在一起。所以它不是一个   看起来很清楚。

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

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