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 是值为 1G_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(遍)在不必创建指令的情况下确定合法性,又限制了谓词可依赖的信息。目前,确定合法性的谓词可用的信息包含

  • 指令的操作码

  • 每个类型索引的类型(参见 type0type1 等)

  • 每个 MachineMemOperand 的字节大小和原子排序

注意

值得研究的另一种替代方案是将 API 推广为使用 std::function 表示操作,该函数实现操作,而不是显式的枚举标记(LegalWidenScalar、...)来指示它调用函数。这将有一些好处,最显著的是可以删除 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,并且此规则指示合法化器将类型 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 是合法的,其他指令被安排由合法化器处理。

规则操作

有各种规则工厂将规则附加到规则集,但它们有一些共同的操作

  • 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 缺乏向量输入的合法性要求。

指针的最低合法性

指针没有最低规则,因为 legalizer 可以将 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 必须是合法的

您可能期望有许多其他操作具有合法性要求,但可以将它们降低为目标指令,这些指令按定义是合法的。