【问题标题】:What is the Implementation of Generics for the NET Common Language RuntimeNET 公共语言运行时的泛型实现是什么
【发布时间】:2011-02-24 03:13:59
【问题描述】:

当您在 C#(或一般的 .NET)中使用泛型集合时,编译器是否基本上完成了开发人员过去必须为特定类型制作泛型集合的繁琐工作。所以基本上。 . .它只是节省了我们的工作量?

现在我想起来,那是不对的。因为没有泛型,我们过去不得不在内部创建使用非泛型数组的集合,因此需要装箱和拆箱(如果它是值类型的集合)等等。

那么,泛型在 CIL 中是如何呈现的?当我们说我们想要一个通用的东西集合时,它在做什么?我不一定想要 CIL 代码示例(尽管这样也可以),我想知道编译器如何获取我们的泛型集合并呈现它们的概念。

谢谢!

附:我知道我可以使用 ildasm 来查看这个,但我的 CIL 对我来说仍然看起来像中国人,我还没有准备好解决这个问题。我只想要 C#(以及我猜的其他语言)如何在 CIL 中呈现以处理泛型的概念。

【问题讨论】:

  • 您的问题并非特定于集合。所有泛型的工作方式都相同,无论它们是否是泛型集合。
  • 好的。我不确定。有人可以编辑问题标题吗?
  • 今天早些时候的这个问题有很多指向回答您问题的文章的链接。 stackoverflow.com/questions/5098335/…
  • 很棒的问答链接。谢谢,埃里克。

标签: c# .net generics cil


【解决方案1】:

请原谅我的冗长帖子,但这个主题相当广泛。我将尝试描述 C# 编译器发出的内容以及 JIT 编译器在运行时如何解释这些内容。

ECMA-335(这是一个写得很好的设计文档;检查一下)是了解一切(我的意思是一切)如何在 .NET 程序集中表示的地方。程序集中的通用信息有一些相关的 CLI 元数据表:

  1. GenericParam - 存储有关通用参数的信息(索引、标志、名称、拥有类型/方法)。
  2. GenericParamConstraint - 存储有关泛型参数约束(拥有泛型参数、约束类型)的信息。
  3. MethodSpec - 存储实例化的通用方法签名(例如 Bar.Method 用于 Bar.Method)。
  4. TypeSpec - 存储实例化的泛型类型签名(例如 Bar for Bar)。

考虑到这一点,让我们通过一个使用此类的简单示例:

class Foo<T>
{
    public T SomeProperty { get; set; }
}

当 C# 编译器编译此示例时,它将在 TypeDef 元数据表中定义 Foo ,就像对任何其他类型一样。与非泛型类型不同,它在 GenericParam 表中也有一个条目,用于描述其泛型参数(索引 = 0,标志 = ?,名称 =(字符串堆的索引,“T”),所有者 = 类型“Foo ")。

TypeDef 表中的一列数据是 MethodDef 表的起始索引,它是在此类型上定义的方法的连续列表。对于 Foo,我们定义了三个方法:SomeProperty 的 getter 和 setter,以及编译器提供的默认构造函数。因此,MethodDef 表将为这些方法中的每一个保留一行。 MethodDef 表中的重要列之一是“签名”列。此列存储对描述该方法的确切签名的字节块的引用。 ECMA-335 非常详细地介绍了这些元数据签名 blob,因此我不会在这里重复这些信息。

方法签名 blob 包含有关参数的类型信息以及返回值。在我们的示例中,setter 接受一个 T,而 getter 返回一个 T。那么,什么是 T?在签名 blob 中,它将是一个特殊值,表示“索引 0 处的泛型类型参数”。这意味着 GenericParams 表中 index=0 且 owner=type "Foo" 的行,即我们的 "T"。

自动属性后备存储字段也是如此。 Foo 在 TypeDef 表中的条目将有一个到 Field 表的起始索引,并且 Field 表有一个“签名”列。该字段的签名将表示该字段的类型是“索引 0 处的泛型类型参数”。

这一切都很好,但是当 T 是不同的类型时,代码生成在哪里发挥作用?实际上,JIT 编译器负责为通用实例生成代码,而不是 C# 编译器。

我们来看一个例子:

Foo<int> f1 = new Foo<int>(); 
f1.SomeProperty = 10;
Foo<string> f2 = new Foo<string>();
f2.SomeProperty = "hello";

这将编译成类似这样的 CIL:

newobj <MemberRefToken1> // new Foo<int>()
stloc.0 // Store in local "f1"
ldloc.0 // Load local "f1"
ldc.i4.s 10 // Load a constant 32-bit integer with value 10
callvirt <MemberRefToken2> // Call f1.set_SomeProperty(10)
newobj <MemberRefToken3> // new Foo<string>()
stloc.1 // Store in local "f2"
ldloc.1 // Load local "f2"
ldstr <StringToken> // Load "hello" (which is in the user string heap)
callvirt <MemberRefToken4> // Call f2.set_SomeProperty("hello")

那么这个 MemberRefToken 业务是什么? MemberRefToken 是一个元数据标记(标记是四个字节值,最高有效字节是元数据表标识符,其余三个字节是行号,从 1 开始)引用 MemberRef 元数据表中的一行。此表存储对方法或字段的引用。在泛型之前,此表将存储有关您正在使用的方法/字段的信息,这些信息来自引用程序集中定义的类型。但是,它也可以用于引用泛型实例化的成员。因此,假设 MemberRefToken1 指的是 MemberRef 表中的第一行。它可能包含以下数据:class= TypeSpecToken1,name = ".ctor",blob =

TypeSpecToken1 将引用 TypeSpec 表中的第一行。从上面我们知道这个表存储了泛型类型的实例。在这种情况下,该行将包含对“Foo”的签名 blob 的引用。所以这个 MemberRefToken1 确实是在说我们正在引用“Foo.ctor()”。

MemberRefToken1MemberRefToken2 将共享相同的类值,即 TypeSpecToken1。但是,它们在名称和签名 blob 上会有所不同(MethodRefToken2 将用于“set_SomeProperty”)。同样,MemberRefToken3MemberRefToken4 将共享 TypeSpecToken2,即 "Foo" 的实例化,但名称和 blob 相同方式。

当 JIT 编译器编译上述 CIL 时,它注意到它看到了一个以前没有见过的通用实例(即 Foo 或 Foo)。 Shiv Kumar 的回答很好地涵盖了接下来发生的事情,所以我不会在这里详细重复。简而言之,当 JIT 编译器遇到一个新的实例化泛型类型时,它可能会在其类型系统中发出一个全新的类型,其字段布局使用实例化中的实际类型代替泛型参数。他们还将拥有自己的方法表,并且每个方法的 JIT 编译将涉及用实例化的实际类型替换对泛型参数的引用。 JIT 编译器还有责任强制执行 CIL 的正确性和可验证性。

总结一下:C# 编译器发出元数据,描述什么是泛型以及泛型类型/方法是如何实例化的。 JIT 编译器使用此信息在运行时为实例化的泛型类型生成新类型(假设它与现有实例化不兼容),并且每种类型都将拥有自己的代码副本,该副本已根据使用的实际类型进行 JIT 编译在实例化中。

希望这在某种程度上是有意义的。

【讨论】:

  • 我正努力接受这个答案。我将尝试绘制您刚刚在此处放置的内容,以便将其可视化。
  • 我很抱歉要消化很多东西。这是一个很难简洁回答的问题:)。
  • 另外,如果您好奇,您可以使用 ildasm.exe 以更友好的方式查看元数据表,方法是转到查看 -> 元数据 -> 显示! (这不是我的想法,可能并不完全准确)。您还可以在反汇编方法时“显示令牌”和“显示字节”。在那里,您将看到用于获取元数据令牌(如 newobj)的指令的 4 字节令牌操作数。 ECMA-335 具有完整的指令参考,并详细说明了每条指令的评估堆栈转换;当我开始学习 CLR 的内部工作原理时,我发现它非常宝贵。
【解决方案2】:

对于值类型,在运行时为每个值类型泛型类定义了一个特定的“类”。对于引用类型,只有一个类定义可以跨不同类型重用。

我在这里简化,但这就是概念。

泛型的设计和实现 NET 公共语言运行时

我们的方案大致如下: 当运行时需要特定的 参数化的实例化 类,加载器检查是否 实例化与任何兼容 它以前见过的;如果没有,那么 现场布局已确定并新建 vtable 已创建,可共享 在所有兼容的实例之间。 此 vtable 中的项目是条目 类方法的存根。 稍后调用这些存根时, 他们将生成(“及时”) 为所有兼容共享代码 实例化。编译时 调用(非虚拟) 特定的多态方法 实例化,我们先检查看看

如果我们之前编译过这样的调用 对于一些兼容的实例化;如果 不是,然后生成一个入口存根, 这将反过来生成代码 为所有兼容共享 实例化。两个实例是 如果对于任何参数化,则兼容 将其编译分类在这些 实例化产生相同的 代码和其他执行结构 (例如字段布局和 GC 表), 除了所描述的字典 见第 4.4 节。尤其, 所有引用类型都兼容 彼此,因为装载机 和 JIT 编译器没有区别 用于现场布局或 代码生成。关于实施 对于 Intel x86,至少是原始的 类型是相互不兼容的,甚至 如果它们具有相同的大小(浮点数和 整数有不同的参数传递 公约)。这留下了用户定义的 结构类型,如果兼容 他们的布局是相同的 垃圾收集,即他们共享 跟踪指针的相同模式。

http://research.microsoft.com/pubs/64031/designandimplementationofgenerics.pdf

【讨论】:

  • 那是一篇很棒的论文。感谢您的链接。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-05-25
  • 1970-01-01
  • 1970-01-01
  • 2010-12-15
  • 2021-09-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多