通用机器 IR

通用 MIR (gMIR) 是一种中间表示,它与 MachineIR (MIR) 共享相同的数据结构,但具有更宽松的约束。 随着编译管道的进行,这些约束逐渐收紧,直到 gMIR 变为 MIR。

本文档的其余部分将假定您熟悉 MachineIR (MIR) 中的概念,并将重点介绍 MIR 和 gMIR 之间的差异。

通用机器指令

注意

本节扩展了 MIR 语言参考中的 机器指令

MIR 主要处理目标指令,并且只有少量目标无关的操作码,例如 COPYPHIREG_SEQUENCE,而 gMIR 定义了丰富的 通用操作码 集合,这些操作码是目标无关的,并描述了通常由目标支持的操作。 一个例子是 G_ADD,它是整数加法的通用操作码。 有关每个通用操作码的更多信息,请参见 通用操作码

MachineIRBuilder 类包装了 MachineInstrBuilder,并提供了一种创建这些通用指令的便捷方法。

通用虚拟寄存器

注意

本节扩展了 MIR 语言参考中的 寄存器

通用虚拟寄存器类似于虚拟寄存器,但它们没有分配寄存器类约束。 相反,通用虚拟寄存器具有较宽松的约束,首先是 底层类型,然后进一步约束到 寄存器组。 最终,它们将被约束到寄存器类,届时它们将变为普通虚拟寄存器。

通用虚拟寄存器可以与 MachineRegisterInfo 提供的所有虚拟寄存器 API 一起使用。 特别是,def-use 链 API 可以使用,而无需将它们与非通用虚拟寄存器区分开来。

为了简单起见,大多数通用指令仅接受虚拟寄存器(包括通用和非通用)。 有一些例外情况,但总的来说

  • 它们不使用立即数,而是使用由实例化立即数值的指令定义的通用虚拟寄存器(请参阅 常量转换)。 通常,这是 G_CONSTANT 或 G_FCONSTANT。 此规则的一个例外是 G_SEXT_INREG,其中必须有立即数。

  • 它们不使用物理寄存器,而是使用通用虚拟寄存器,该寄存器由来自物理寄存器的 COPY 定义,或由定义物理寄存器的 COPY 使用。

历史记录

我们从另一种表示形式开始,其中 MRI 跟踪每个通用虚拟寄存器的大小,而指令具有类型列表。 这有两个缺陷:类型和大小是冗余的,并且没有通用的方法来获取给定操作数的类型(因为指令类型和操作数之间没有 1:1 的映射)。 我们考虑将类型放在 MCInstrDesc 的某种变体中:请参阅 PR26576: [GlobalISel] Generic MachineInstrs need a type 但这增加了相关对象的内存占用

寄存器组

寄存器组是由目标定义的一组寄存器类。 此定义相当宽松,因此让我们讨论一下它们可以实现什么。

假设我们有一个处理器,它有两个寄存器文件 A 和 B。 它们在各方面都相同,并且以相同的成本支持相同的指令。 它们只是物理上分开存储,并且每条指令只能访问来自 A 或 B 的寄存器,但永远不会混合两者。 如果我们想对分散在两个寄存器文件中的数据执行操作,我们必须首先将所有数据复制到一个寄存器文件中。

给定像这样的处理器,我们将受益于将相关数据聚集到一个寄存器文件中,以便最大限度地减少来回复制数据的成本,以满足所有指令(可能冲突)的要求。 寄存器组是一种约束寄存器分配器为虚拟寄存器使用特定寄存器文件的方法。

实际上,寄存器文件 A 和 B 很少相等。 它们通常可以存储相同的数据,但通常对您可以在每个寄存器文件上执行的操作有一些限制。 一个相当常见的模式是其中一个可用于整数运算,另一个可用于浮点运算。 为了适应这一点,让我们将 A 和 B 重命名为 GPR(通用寄存器)和 FPR(浮点寄存器)。

我们现在有一些额外的限制来限制我们。 像 G_FMUL 这样的操作必须在 FPR 中发生,而 G_ADD 必须在 GPR 中发生。 然而,即使这规定了很多分配,我们仍然有一些自由。 G_LOAD 可以在 GPR 和 FPR 中发生,我们想要哪个取决于谁将消耗加载的数据。 类似地,G_FNEG 可以在 GPR 和 FPR 中发生。 如果我们将其分配给 FPR,那么我们将使用浮点求反。 但是,如果我们将其分配给 GPR,那么我们可以等效地将符号位与 1 进行 G_XOR 运算以反转它。

总而言之,寄存器组是一种基于对在给定上下文中应用每个选择时差异的分析,来消除表面上等效的选择之间歧义的方法。

为了给出一些具体的例子

AArch64

AArch64 有三个主要的组。 GPR 用于整数运算,FPR 用于浮点运算以及 NEON 向量指令集。 第三个是 CCR,它描述了用于谓词的条件代码寄存器。

MIPS

MIPS 有五个主要的组,其中许多程序实际上只使用一个或两个。 GPR 是用于整数运算的通用组。 FGR 或 CP1 用于浮点运算以及 MSA 向量指令和一些其他特定于应用程序的扩展。 CP0 用于系统寄存器,很少有程序会使用它。 CP2 和 CP3 用于芯片中可能存在的任何特定于应用程序的协处理器。 可以说,还有第六个用于 LO 和 HI 寄存器,但这些寄存器仅用于少数操作的结果,并且从 GPR 中单独建模的价值值得怀疑。

X86

X86 可以看作有 3 个主要的组:通用、x87 和向量(可以进一步拆分为每个域的组,用于单精度与双精度指令)。 看起来也可能有更多潜在的组,例如用于 AVX512 掩码寄存器的组。

寄存器组由目标提供的 API RegisterBankInfo 描述。

底层类型

此外,每个通用虚拟寄存器都有一个类型,由 LLT 类的实例表示。

EVT/MVT/Type 类似,它不区分无符号和有符号整数类型。 此外,它也不区分整数和浮点类型:它主要传达绝对必要的信息,例如大小和向量通道数

  • sN 用于标量

  • pN 用于指针

  • <N x sM> 用于向量

LLT 旨在取代 SelectionDAG 中 EVT 的用法。

以下是一些 LLT 示例及其 EVTType 等效项

LLT

EVT

IR 类型

s1

i1

i1

s8

i8

i8

s32

i32

i32

s32

f32

float

s17

i17

i17

s16

N/A

{i8, i8} [1]

s32

N/A

[4 x i8] [1]

p0

iPTR

i8*, i32*, %opaque*

p2

iPTR

i8 addrspace(2)*

<4 x s32>

v4f32

<4 x float>

s64

v1f64

<1 x double>

<3 x s32>

v3i32

<3 x i32>

理由:指令已经编码了类型的特定解释(例如,addfadd,或 sdivudiv)。 也在类型系统中编码该信息需要引入 bitcast,但对于选择器而言没有实际优势。

指针类型通过地址空间区分。 这与 IR 匹配,与 SelectionDAG 不同,在 SelectionDAG 中,地址空间是操作的属性。 这种表示形式更好地支持指针根据其地址空间具有不同的大小。

注意

注意

这仍然正确吗? 我以为我们已经删除了 1 元素向量的概念。 理论上,它可能与标量不同,但我认为我们未能找到真正的实例。

目前,LLT 要求向量中至少有 2 个元素,但某些目标具有“1 元素向量”的概念。 将它们表示为其底层标量类型是一种很好的简化。

脚注

通用操作码参考

可用的通用操作码在 通用操作码 中描述。