Legalizer¶
此 Pass 将通用机器指令转换为合法指令。
合法指令的定义如下:
可选的 — 目标稍后将能够将其选择为目标特定的(非通用)指令。但这并不一定意味着 InstructionSelect 必须处理它。它仅仅意味着 **某些东西** 必须处理它。
在可加载和存储的 vreg 上操作 – 如果必要,目标可以选择每个 gvreg 操作数的
G_LOAD
/G_STORE
。
与 SelectionDAG 相反,没有合法化阶段。特别是,“类型”和“操作”合法化不是分开的。
合法化是迭代的,所有状态都包含在 GMIR 中。为了维护中间代码的有效性,引入了指令
G_MERGE_VALUES
— 将多个相同大小的寄存器连接成一个更宽的寄存器。G_UNMERGE_VALUES
— 从一个更宽的寄存器中提取多个相同大小的寄存器。G_EXTRACT
— 从一个更宽的寄存器中提取一个简单的寄存器(作为连续的位序列)。
由于它们预计是合法化过程中的临时副产品,因此它们在 Legalizer Pass 结束时被组合在一起。如果任何指令仍然存在,则预计它们始终是可选的,如果必要,可以使用加载和存储。
指令的合法性可能仅取决于指令本身,并且不得依赖于使用指令的任何上下文。但是,在确定指令不合法后,可以使用指令的上下文来决定如何使指令合法化。例如,如果我们有一个 G_FOO
指令,其形式为
%1:_(s32) = G_CONSTANT i32 1
%2:_(s32) = G_FOO %0:_(s32), %1:_(s32)
不可能说 G_FOO 是合法的当且仅当 %1 是值为 1
的 G_CONSTANT
。但是,以下内容
%2:_(s32) = G_FOO %0:_(s32), i32 1
可以说明它是合法的,当且仅当操作数 2 是值为 1
的立即数,因为该信息完全包含在单个指令中。
API:LegalizerInfo¶
推荐的 [1] API 如下所示
getActionDefinitionsBuilder({G_ADD, G_SUB, G_MUL, G_AND, G_OR, G_XOR, G_SHL})
.legalFor({s32, s64, v2s32, v4s32, v2s64})
.clampScalar(0, s32, s64)
.widenScalarToNextPow2(0)
.clampNumElements(0, v2s32, v4s32)
.clampNumElements(0, v2s64, v2s64)
.moreElementsToNextPow2(0);
并描述了一组规则,通过这些规则,我们可以声明指令合法或决定采取哪些措施使其更合法。
此规则集的核心是 LegalityQuery
,它描述了指令。我们使用描述而不是指令,既是为了允许其他 Pass 在无需创建指令的情况下确定合法性,也是为了将谓词可用的信息限制在可以安全依赖的信息范围内。目前,确定合法性的谓词可用的信息包括
指令的操作码
每个类型索引的类型(参见
type0
、type1
等)每个 MachineMemOperand 的大小(以字节为单位)和原子排序
注意
值得研究的另一种方法是将 API 泛化为使用 std::function
表示操作,该函数实现操作,而不是显式枚举标记(Legal
、WidenScalar
等)来指示它调用函数。这将带来一些好处,最显著的是可以删除 Custom。
脚注
规则处理和声明规则¶
getActionDefinitionsBuilder
函数为给定的操作码生成一个规则集,可以向其中添加规则。如果给出了多个操作码,则它们都永久绑定到同一个规则集。规则集中的规则从上到下执行,如果指令因规则而合法化,则将从顶部重新开始。如果规则集耗尽而没有满足任何规则,则认为它不受支持。
当它没有声明指令合法时,每次遍历规则都可能请求一种类型更改为另一种类型。有时这会导致多种类型发生变化,但我们尽量避免这种情况,因为进行多次更改可能难以避免无限循环,例如,缩小一种类型会导致另一种类型太小,而扩大该类型会导致第一种类型太大。
通常,建议在规则顶部尽可能靠近的地方声明指令合法,并将任何昂贵的规则放在尽可能低的位置。这有助于提高性能,因为测试合法性的频率高于合法化,而合法化可能需要多次遍历规则。
举一个具体的例子,考虑规则
getActionDefinitionsBuilder({G_ADD, G_SUB, G_MUL, G_AND, G_OR, G_XOR, G_SHL})
.legalFor({s32, s64, v2s32, v4s32, v2s64})
.clampScalar(0, s32, s64)
.widenScalarToNextPow2(0);
以及指令
%2:_(s7) = G_ADD %0:_(s7), %1:_(s7)
它不满足 .legalFor() 的谓词,因为 s7
不在列出的类型中,因此它会继续执行 .clampScalar()。它确实满足此规则的谓词,因为该类型小于 s32
,并且此规则指示 Legalizer 将类型 0 更改为 s32
。然后它从顶部重新开始。这次它确实满足了 .legalFor()
,并且结果输出为
%3:_(s32) = G_ANYEXT %0:_(s7)
%4:_(s32) = G_ANYEXT %1:_(s7)
%5:_(s32) = G_ADD %3:_(s32), %4:_(s32)
%2:_(s7) = G_TRUNC %5:_(s32)
其中 G_ADD
是合法的,其他指令安排由 Legalizer 处理。
规则操作¶
有各种规则工厂将规则附加到规则集中,但它们有一些共同的操作
legalIf()
、legalFor()
等,如果谓词满足,则声明指令合法。narrowScalarIf()
、narrowScalarFor()
等,如果谓词满足,则声明指令不合法,并指示将其中一种类型中的标量缩小到特定类型将使其更合法。此操作支持标量和向量。widenScalarIf()
、widenScalarFor()
等,如果谓词满足,则声明指令不合法,并指示将其中一种类型中的标量扩展到特定类型将使其更合法。此操作支持标量和向量。fewerElementsIf()
、fewerElementsFor()
等,如果谓词满足,则声明指令不合法,并指示将其中一种类型的向量元素数量减少到特定类型将使其更合法。此操作支持向量。moreElementsIf()
、moreElementsFor()
等,如果谓词满足,则声明指令不合法,并指示将其中一种类型的向量元素数量增加到特定类型将使其更合法。此操作支持向量。lowerIf()
、lowerFor()
等,如果谓词满足,则声明指令不合法,并指示用等效指令替换它将使其更合法。每个操作码对此操作的支持不同。这些可能提供一个可选的 LegalizeMutation,其中包含一个类型,以尝试在不同的类型中执行扩展。libcallIf()
、libcallFor()
等,如果谓词满足,则声明指令不合法,并指示用 libcall 替换它将使其更合法。每个操作码对此操作的支持不同。customIf()
、customFor()
等,如果谓词满足,则声明指令不合法,并指示后端开发人员将提供使其更合法的方法。unsupportedIf()
、unsupportedFor()
等,如果谓词满足,则声明指令不合法,并指示无法使其合法,并且编译器应该失败。fallback()
回退到旧的 API,并且仅应在将现有代码从该 API 移植时使用。
规则谓词¶
规则工厂也有一些共同的谓词
legal()
、lower()
等始终满足。legalIf()
、narrowScalarIf()
等,如果用户提供的LegalityPredicate
函数返回 true,则满足。此谓词可以访问LegalityQuery
中的信息以做出决策。用户提供的谓词也可以使用all(P0, P1, ...)
组合。legalFor()
、narrowScalarFor()
等,如果类型匹配给定类型集合中的一个,则满足条件。例如.legalFor({s16, s32})
声明如果类型 0 为 s16 或 s32,则指令合法。通常也提供针对两个和三个类型索引的版本。对于这些版本,所有一起考虑的类型索引必须与其中一个元组中的所有类型匹配。因此,.legalFor({{s16, s32}, {s32, s64}})
仅接受{s16, s32}
或{s32, s64}
,但不会接受{s16, s64}
。legalForTypesWithMemSize()
、narrowScalarForTypesWithMemSize()
等类似于legalFor()
、narrowScalarFor()
等,但此外还需要 MachineMemOperand 在每个元组中具有给定大小。legalForCartesianProduct()
、narrowScalarForCartesianProduct()
等,如果每个类型索引与每个独立集合中的一个元素匹配,则满足条件。因此,.legalForCartesianProduct({s16, s32}, {s32, s64})
将接受{s16, s32}
、{s16, s64}
、{s32, s32}
和{s32, s64}
。
复合规则¶
有一些针对常见情况的复合规则,它们基于上述功能构建而成。
widenScalarToNextPow2()
类似于widenScalarIf()
,但当且仅当类型大小(以位为单位)不是 2 的幂时才满足条件,并选择下一个最大的 2 的幂作为目标类型。
minScalar()
类似于widenScalarIf()
,但当且仅当类型大小(以位为单位)小于给定的最小值时才满足条件,并选择最小值作为目标类型。类似地,还有maxScalar()
用于最大值,以及clampScalar()
用于同时执行两者。minScalarSameAs()
类似于minScalar()
,但最小值取自另一个类型索引。moreElementsToNextMultiple()
类似于moreElementsToNextPow2()
,但基于 X 的倍数而不是 2 的幂。
最小规则集¶
GlobalISel 的合法化器在给定目标如何塑造后端其余部分必须处理的 GMIR 方面具有很大的灵活性。但是,所有目标都必须满足少量要求。
在讨论最低要求之前,我们需要一些术语。
- 生产者类型集
至少一条合法指令产生的所有可能类型的并集。
- 消费者类型集
至少一条合法指令使用的所有可能类型的并集。
这两个集合通常是相同的,但没有保证。例如,无法使用 s64 但仍然能够为少数特定指令生成它的情况并不少见。
标量的最小规则¶
对于来自生产者类型集的所有输入和来自消费者类型集的所有更大输出,G_ANYEXT 必须合法。
对于来自生产者类型集的所有输入和来自消费者类型集的所有更小输出,G_TRUNC 必须合法。
G_ANYEXT 和 G_TRUNC 具有强制合法性,因为 GMIR 需要一种连接不同类型大小的操作的方法。它们通常很容易支持,因为 G_ANYEXT 没有定义附加位的数值,而 G_TRUNC 正在丢弃位。其他转换可以通过一些其他操作(受进一步合法化影响)降低到 G_ANYEXT/G_TRUNC。例如,G_SEXT 可以降低到
%1 = G_ANYEXT %0
%2 = G_CONSTANT ...
%3 = G_SHL %1, %2
%4 = G_ASHR %3, %2
并且 G_CONSTANT/G_SHL/G_ASHR 可以进一步降低到其他操作或目标指令。类似地,G_FPEXT 没有合法性要求,因为它可以降低到 G_ANYEXT 后跟目标指令。
G_MERGE_VALUES 和 G_UNMERGE_VALUES 没有合法性要求,因为前者可以降低到 G_ANYEXT 和一些其他可合法化的指令,而后者可以降低到一些可合法化的指令后跟 G_TRUNC。
向量的最小合法性¶
在向量类型中,LLVM IR 中没有定义转换,因为向量通常通过重新解释位或通过分解向量并将其重构为不同的类型来转换。因此,G_BITCAST 是唯一需要考虑的操作。我们通常不需要它合法,因为它通常可以降低到 COPY(或使用 replaceAllUses() 降低到无)。但是,在某些情况下,G_BITCAST 并非微不足道(例如,大端数据的低端向量,例如在大端 MIPS MSA 和大端 ARM NEON 上,请参见 _i_bitcast)。为了解决这个问题,G_BITCAST 对于更改值中位模式的所有类型组合必须合法。
G_BUILD_VECTOR 或 G_BUILD_VECTOR_TRUNC 没有合法性要求,因为它们可以通过以下方式处理:* 声明它们合法。* 将它们标量化。* 将它们降低到 G_TRUNC+G_ANYEXT 和一些可合法化的指令。* 将它们降低到根据定义是合法的目标指令。
同样的推理也允许 G_UNMERGE_VALUES 对于向量输入缺乏合法性要求。
指针的最小合法性¶
指针没有最小规则,因为 G_INTTOPTR 和 G_PTRTOINT 可以由合法化器选择为从寄存器类到另一个寄存器类的 COPY。
操作的最小合法性¶
G_ANYEXT、G_MERGE_VALUES、G_BITCAST、G_BUILD_VECTOR、G_BUILD_VECTOR_TRUNC、G_CONCAT_VECTORS、G_UNMERGE_VALUES、G_PTRTOINT 和 G_INTTOPTR 的规则已在上面指出。除了这些之外,以下操作还有要求
至少一个 G_IMPLICIT_DEF 必须合法。这通常很简单,因为它不需要选择任何代码。
G_PHI 对于生产者和消费者类型集中的所有类型都必须合法。这通常很简单,因为它不需要选择任何代码。
至少一个 G_FRAME_INDEX 必须合法。
至少一个 G_BLOCK_ADDR 必须合法。
您期望有许多其他操作具有合法性要求,但它们可以降低到根据定义是合法的目标指令。