Go 1.18中如何实现泛型

2022-03-02 05:02:50

本文档描述了Go 1.18中通过字典和GCShape模板的泛型实现。它提供了比Gcshape设计文档中描述的更具体和最新的信息

泛型的编译器实现(在类型检查之后)主要关注于创建泛型函数和方法的实例化,这些函数和方法将使用具有具体类型的参数执行。为了避免为具有不同类型参数的泛型函数/方法的每次调用创建不同的函数实例化(这将是纯粹的模板化),我们在每次调用泛型函数/方法时都会传递一个字典。字典提供了有关类型参数的相关信息,这些信息允许单个函数实例化为许多不同的类型参数正确运行。

然而,为了实现的简单性(和性能),对于所有可能的类型参数,我们没有单一的泛型函数/方法编译。相反,我们在具有相同gcshape的类型参数集之间共享泛型函数/方法的实例化。

gcshape(或gcshape grouping)是一组类型,当指定为类型参数之一时,这些类型在我们的实现中可以共享通用函数/方法的相同实例化。因此,例如,对于具有单个类型参数的泛型函数,对于同一gcshape分组中的所有类型参数,我们只需要一个函数实例化。类似地,对于具有单个类型参数的泛型类型的方法,我们只需要对同一gcshape分组中的所有类型参数(泛型类型)进行一次实例化。gcshape类型是我们在此类实例化的实现中使用的特定类型,用于填充gcshape分组的所有类型。

我们目前正在以一种相当细粒度的方式实现gcshapes。当且仅当两个具体类型具有相同的基础类型或它们都是指针类型时,它们才处于相同的gcshape分组中。我们有意定义gcshapes,这样就不需要在字典中包含任何运算符方法(例如,为指定类型arg实现“+”运算符)。特别是,本质上不同的内置类型(如int和float64)绝不是同一个形状。即使是int16和int32也有不同的操作(尤其是左移位和右移位),所以我们不把它们放在同一个形状中。类似地,我们希望gcshape中的所有类型始终以相同的方式实现内置方法(例如make/new/len)。我们可以在同一个gcshape中包含一些密切相关的内置类型(如uint和uintptr),但目前还没有这样做。我们目前的细粒度gcshape已经暗示了这一点,但我们也总是希望界面类型与非界面类型处于不同的gcshape中(即使非界面类型与界面类型具有相同的双场结构)。接口类型在调用方法等方面与非接口类型的行为非常不同。

目前,我们根据基础类型的唯一字符串表示(在types.LinkString中实现)来命名每个gcshape类型。我们将所有形状类型放入一个独特的内置包“go.shape”。出于实现原因(请参见下一节),我们碰巧在gcshape类型的名称中包含了类型参数列表中gcshape参数的索引。因此,基础类型为“string”的类型将对应于名为“go.shape.string_0”或“go.shape.string_1”的gcshape类型,具体取决于该类型是用作泛型函数或类型的第一个类型参数还是第二个类型参数。所有指针类型都是以单个示例类型*uint8命名的,因此指针形状的gcshapes名称为go。形状*快去。形状*uint8_1等。

我们将一组特定形状类型参数的泛型函数或方法的实例化称为形状实例化。

每个字典都是在编译时静态定义的。字典对应于程序中的调用站点,其中使用一组特定的具体类型参数调用特定的泛型函数/方法。无论是从非泛型函数还是泛型函数/方法调用,每当调用泛型函数/方法时都需要字典。字典当前以被调用的完全限定的泛型函数/方法名和具体类型参数的名称命名。主要有两个字典名示例。。dict.Map[int,bool]和main。。dict.mapCons[int,bool]。应用)。这些是调用或引用main所需的词典。映射[int,bool]()和rcvr。Apply(),其中rcvr具有main类型。mapCons[int,bool]。字典包含使用这些具体类型参数执行基于gcshape的泛型函数/方法实例化所需的信息。具有相同名称的词典被完全消除重复(通过编译器和链接器的某种组合)。

通过分析泛型函数/方法的形状实例化,我们可以收集有关字典预期格式的信息。我们分析一个实例化,而不是泛型函数/方法本身,因为所需的字典条目可能取决于形状参数——尤其是形状参数是否是接口类型。重要的是,实例化已经“转换”到所有隐式接口转换(oconvidace)都显式的程度。显式或隐式接口转换(尤其是转换为非空接口)可能需要字典中的额外条目。

为了创建字典条目,我们通常需要用与字典关联的实际类型参数替换形状类型参数。因此,即使多个类型参数恰好具有相同的形状(例如,它们都是指针类型),形状类型参数也必须是完全可区分的。因此,如上所述,我们实际上将type参数的索引添加到shape类型中,这样就可以完全正确地区分不同的类型参数。

所有(或所需)派生类型的列表,这些派生类型出现在泛型函数/方法中,或以某种方式隐式出现在泛型函数/方法中,用具体的类型参数替换。也就是说,从函数/方法的类型参数(例如.*T、[]T、map[K、V]等)中具体派生并以某种方式在泛型函数/方法中使用的具体类型列表。

目前,我们在需要表达式的运行时类型的情况下使用派生类型。这些情况包括到空接口的显式或隐式转换,以及类型断言和类型开关,其中源值的类型是空接口。

在运行时,调试器还使用派生类型和类型参数项来确定参数和局部变量的具体类型。在编译时,有关类型参数和派生类型字典项的信息将与DWARF信息一起发出。对于具有参数化类型的每个参数或局部变量,DWARF信息还指示将包含参数或变量具体类型的字典条目。

所有子字典的列表:泛型函数/方法内部的泛型函数/方法调用需要一个子字典,其中内部调用的类型参数取决于外部函数的类型参数。对于引用泛型函数/方法的函数/方法值和方法表达式,同样需要子字典。

子字典条目指向使用所需类型参数执行被调用函数/方法所需的普通顶级字典,使用外部函数字典的类型参数进行替换。

从类型参数或派生类型转换为特定非空接口所需的任何特定ITAB。目前,我们使用字典派生的ITAB主要有四种情况。在所有情况下,itab都必须来自字典,因为它取决于当前函数的类型参数。用于从非接口类型到非空接口的所有显式或隐式OConvidace调用。itab用于创建目标接口。

对于类型参数的所有方法调用(必须是类型参数绑定中的方法)。此方法调用作为接收方到类型绑定接口的转换来实现,因此处理方式类似于隐式oconvidace调用。

用于从非空接口到非接口类型的所有类型断言。需要itab来实现类型断言。

对于涉及从类型参数派生的非接口类型的类型切换情况,其中打开的值具有非空接口类型。与类型断言一样,需要itab来实现类型切换。

我们已经决定,引用泛型值/类型的泛型函数/方法中的闭包应该使用与其包含的函数/方法相同的字典。因此,一个实例化函数/方法的字典应该包括它所包含的闭包的所有主体所需的所有条目。

当前实现可能有重复的子词典条目和/或重复的itab条目。通过在实现过程中多做一些工作,这些条目显然可以消除重复并共享。对于一些不寻常的情况,可能还有一些未使用的字典条目可以优化掉。

我们选择在编译时计算所有字典和子字典,这确实意味着有些程序无法运行。对于具有特定具体类型的泛型函数/方法的每个可能实例化,我们必须有一个字典。因为我们要求所有字典都在编译时静态创建,所以必须有一组有限的已知类型用于创建函数/方法实例化。因此,我们无法处理通过泛型函数/方法的递归可以创建无限多个不同类型(通常通过重复嵌套泛型类型)的程序。一个典型的例子见第48018期。这些类型的程序通常被称为非单态程序。如果我们能够在运行时动态创建字典(以及泛型类型的实例化),那么我们可能能够处理这些不可单态代码的情况。

为一组特定的gcshape类型参数创建泛型函数或泛型类型方法的编译时实例化。如上所述,我们有时将这种实例化称为形状实例化。我们在编译过程中动态地确定需要创建哪些形状实例化,如下面的“调用泛型函数和方法的编译器处理”中所述。给定一组gcshape类型参数,我们通过在函数/方法体和头中用shape类型参数替换相应的类型参数来创建实例化的函数或方法。函数体包括函数中包含的任何闭包。

在替换过程中,我们还“变换”任何相关节点。旧的typechecker(typecheck包)不仅确定了函数或声明中每个节点的类型,还对代码进行了各种转换,通常转换为更具体的节点操作,还为任何隐式操作(如转换)生成显式节点。在知道操作数的确切类型之前,通常无法进行这些转换。因此,我们会在点头过程中延迟将这些转换应用于泛型函数。相反,我们在进行类型替换以创建实例化时应用转换。其中许多转换包括添加隐式OConvidace节点。在确定实例化的字典格式之前,显式表示所有OConvidace节点非常重要。

在创建实例化的函数/方法时,我们还会自动添加一个字典参数“.dict”作为第一个参数,甚至在方法接收者之前。

我们有一个在这个包编译期间已经创建的形状实例化的哈希表,所以我们不需要重复创建相同的实例化。除了实例化的函数本身,我们还保存了下面描述的字典传递所需的一些额外信息。这包括与实例化相关联的字典的格式,以及只能从泛型函数(例如类型参数的边界)访问或很难直接从实例化体访问的其他信息。我们计算这些额外的信息(字典格式等),作为创建实例化的最后一步。

在编译器中,泛型和实例化函数和方法的命名如下:

泛型方法-方法定义中使用的带有类型参数的接收器类型,以及方法名称,例如(*value[T])。设置(提醒一下,除了接收方类型的类型参数外,方法不能有任何额外的类型参数。)

实例化方法——带有类型参数的接收方类型和方法名称,例如(*value[int])。设置或(*值[go.shape.string_0])。设置

目前,由于编译器仅使用字典(从不使用纯模板),因此通常出现在可执行文件中的唯一函数名是由形状类型实例化的函数和方法。如果所需的Itab必须包含对这些完全实例化方法的引用,则可能会出现一些由具体类型实例化的方法(请参阅下面的";Itab字典包装器";部分)

字典的命名类似于相关的实例化函数或方法,但带有“.dict”前缀。例如:。dict.Max[float64]和。指令(*值[int])。收到字典总是为一组具体的类型定义的,因此字典名称中从来没有任何类型参数或形状类型。

实例化的函数和方法名中包含的具体类型名,以及字典名,都是完全指定的(包括包名,如果不是内置包的话)。因此,实例化函数、实例化方法和字典名称是唯一指定的。因此,它们可以根据需要在任何包中按需生成,链接器将自动消除同一函数、方法或字典的多个实例的重复。

对于泛型函数或泛型类型方法的直接调用,编译器在调用适当的形状实例化时会自动添加额外的初始参数,这是必需的字典。该字典可以是对静态字典(如果具体类型是静态已知的)的引用,也可以是对包含函数字典的子字典的引用。如果创建了函数值、方法值或方法表达式,那么编译器将自动创建一个闭包,在调用函数或方法值或方法表达式时,使用正确的字典调用相应的形状实例化。在生成完全实例化泛型类型的itab的每个条目时,需要一个类似的闭包包装器,因为itab条目必须是一个接受适当的接收方和其他参数的函数,而不是字典。

Types2 typechecker(新)-Types2 typechecker是一种新的typechecker,可以完成通用程序的验证和类型检查。它被编写为独立于编译器的其余部分,并以一组映射的形式将其计算的类型检查信息传递给编译器的其余部分。

节点通行证(预先存在,但完全重写以使用type2 typechecker信息)——节点通行证创建ir。当前包中所有函数/方法的节点表示形式。我们为泛型和非泛型函数创建节点表示。我们使用来自types2 typechecker的信息来设置每个节点的类型。泛型函数中的各种节点可能具有依赖于类型参数的类型。对于非泛型函数,我们进行与旧的typechecker相关的常规转换,如上所述。我们不会对泛型函数进行转换,因为许多转换都依赖于具体的类型信息。

在点头过程中,我们记录源代码中已经存在的每个完全实例化的非接口类型。例如,任何函数(泛型或非泛型)都可能碰巧指定“List[int]”类型的变量。我们在导入所需的函数体时也会做同样的事情(要么因为它是将被实例化的通用函数,要么因为它是内联所需的)。

可导出泛型函数的主体始终是导出的,因为导出的泛型函数可能会被调用,因此需要在引用它的任何其他包中实例化。类似地,可导出泛型类型的方法体也总是导出的,因为我们需要在实例化泛型类型时实例化这些方法。如果未报告的泛型函数和类型被可内联函数引用,则可能需要导出它们(请参见crawler.go)

扫描传递(新)-传递所有非泛型函数和实例化函数,以查找泛型函数/方法的引用。在任何这样的引用中,它都会创建所需的形状实例化(如果在当前编译期间尚未创建),并将引用转换为使用形状实例化并传递到相应的字典中。扫描过程会在所有新创建的实例化函数/方法上重复执行,直到不再创建实例化为止。

在扫描过程的每次迭代开始时,我们为每个完全实例化的类型创建所需的所有实例化方法和字典,这些类型是自扫描过程的最后一次迭代(或者在扫描过程的第一次迭代中,从节点过程)以来看到的。这确保了在创建运行时类型描述符和ITAB(包括字典中所需的ITAB)时,所需的方法实例化可用。

对于正在扫描的函数中对泛型函数/方法的每个引用,我们确定类型参数的GC形状。如果我们还没有用这些形状参数创建所需的实例化,我们可以通过替换泛型函数头和函数体上的类型来创建实例化。泛型函数可能来自另一个包,在这种情况下,我们需要导入它的函数体。一旦我们创建了实例化,我们就可以确定关联字典的格式。我们用调用(可能在闭包中)替换对泛型函数/方法的引用,用所需的字典参数调用所需的实例化。如果引用位于非泛型函数中,那么所需的dictionary参数将是顶级静态dictionary。如果引用在形状实例化中,那么dictionary参数将是包含函数的dictionary中的子dictionary条目。我们根据需要使用字典格式信息计算顶级字典(以及它们所需的所有子字典,递归)。

与节点传递一样,我们记录创建的任何新的完全实例化的非接口类型。在扫描过程中,由于类型替换,将创建此类型。通常,它将用于派生类型的字典条目。如果我们在某些情况下使用纯模板,那么在纯模板函数(没有字典)中创建具体类型时也会发生类似的情况。

字典传递(new)——传递所有实例化函数/方法,转换需要字典条目的操作。这些操作包括对类型参数绑定的方法的调用、参数化类型到接口的转换,以及参数化类型上的类型断言和类型开关。这

......