代码转换元数据

概述

LLVM 转换 Pass 可以通过将元数据附加到要转换的代码来控制。默认情况下,转换 Pass 使用启发式方法来确定是否执行转换,以及在执行转换时如何应用转换的其他细节(例如,选择哪个向量化因子)。除非优化器另有指示,否则转换将以保守的方式应用。这种保守性通常允许优化器避免无利可图的转换,但在实践中,这会导致优化器不应用本来会非常有利的转换。

前端可以向 LLVM Pass 提供有关它们应该应用哪些转换的其他提示。这可能是无法从发出的 IR 中推导出的额外知识,或者是从用户/程序员传递的指令。OpenMP 编译指示是后者的一个例子。

如果从程序中删除了任何此类元数据,则代码的语义不得更改。

循环上的元数据

属性可以附加到循环上,如‘llvm.loop’中所述。属性可以描述循环的属性、禁用转换、强制特定转换以及设置转换选项。

由于元数据节点是不可变的(除了MDNode::replaceOperandWith,它在唯一化元数据上使用很危险),因此为了添加或删除循环属性,必须创建一个新的MDNode并将其分配为新的llvm.loop元数据。旧MDNode与循环之间的任何连接都会丢失。llvm.loop节点也用作 LoopID(Loop::getLoopID()),即循环有效地获得了一个新的标识符。例如,llvm.mem.parallel_loop_access引用 LoopID。因此,如果在添加/删除循环属性后要保留并行访问属性,则任何llvm.mem.parallel_loop_access引用都必须更新为新的 LoopID。

转换元数据结构

某些属性描述代码转换(展开、向量化、循环分布等)。它们可以是向优化器提示转换可能是有益的、指示使用特定选项的指令,或者传达来自用户的特定请求(例如#pragma clang loop#pragma omp simd)。

如果强制进行转换但由于任何原因都无法执行,则必须发出优化未命中警告。语义信息(例如转换是安全的(例如llvm.mem.parallel_loop_access))可以被优化器未使用而不会生成警告。

除非明确禁用,否则任何优化 Pass 都可以启发式地确定转换是否有益并应用它。如果指定了另一个转换的元数据,则在它之前应用不同的转换可能是无意的,因为是在不同的循环上应用的或循环不再存在。为了避免必须显式禁用数量未知的 Pass,属性llvm.loop.disable_nonforced会禁用所有可选的高级重构转换。

以下示例避免在循环被向量化之前被更改,例如被展开。

  br i1 %exitcond, label %for.exit, label %for.header, !llvm.loop !0
...
!0 = distinct !{!0, !1, !2}
!1 = !{!"llvm.loop.vectorize.enable", i1 true}
!2 = !{!"llvm.loop.disable_nonforced"}

应用转换后,会在转换后的和/或新的循环上设置后续属性。这允许指定其他属性,包括后续转换。在同一个元数据节点中指定多个转换是为了兼容性,但它们的执行顺序未定义。例如,当同时指定llvm.loop.vectorize.enablellvm.loop.unroll.enable时,展开可能发生在向量化之前或之后。

例如,以下指令指示循环先向量化,然后再展开。

!0 = distinct !{!0, !1, !2, !3}
!1 = !{!"llvm.loop.vectorize.enable", i1 true}
!2 = !{!"llvm.loop.disable_nonforced"}
!3 = !{!"llvm.loop.vectorize.followup_vectorized", !{"llvm.loop.unroll.enable"}}

如果且仅当没有指定后续操作时,Pass 本身可能会添加属性。例如,向量化器会添加llvm.loop.isvectorized属性以及原始循环的所有属性,但不包括其循环向量化属性。要避免这种情况,可以使用空的后续属性,例如:

!3 = !{!"llvm.loop.vectorize.followup_vectorized"}

无法应用的转换的后续属性永远不会添加到循环中,因此实际上会被忽略。这意味着此类属性中的任何后续转换都需要在其之前的转换应用于后续转换之前。如果强制进行转换,则用户应收到有关转换链中第一个无法应用的转换的警告。所有后续转换都将跳过。

特定于 Pass 的转换元数据

转换选项特定于每个转换。在下面,我们介绍了每个 LLVM 循环优化 Pass 的模型以及影响它们的方法。

循环向量化和交织

循环向量化和交织被解释为单个转换。如果设置了!{"llvm.loop.vectorize.enable", i1 true},则将其解释为强制转换。

假设向量化前的循环是

for (int i = 0; i < n; i+=1) // original loop
  Stmt(i);

则向量化后的代码将近似于(假设 SIMD 宽度为 4)

int i = 0;
if (rtc) {
  for (; i + 3 < n; i+=4) // vectorized/interleaved loop
    Stmt(i:i+3);
}
for (; i < n; i+=1) // epilogue loop
  Stmt(i);

其中rtc是生成的运行时检查。

llvm.loop.vectorize.followup_vectorized将为向量化后的循环设置属性。如果未指定,则llvm.loop.isvectorized与原始循环的属性结合,以避免它被多次向量化。

llvm.loop.vectorize.followup_epilogue将为剩余循环设置属性。如果未指定,它将具有原始循环的属性以及llvm.loop.isvectorizedllvm.loop.unroll.runtime.disable(除非原始循环已经具有展开元数据)。

llvm.loop.vectorize.followup_all指定的属性将添加到这两个循环中。

当使用后续属性时,它会替换为生成的循环所自动推断的任何属性。因此,建议将llvm.loop.isvectorized添加到llvm.loop.vectorize.followup_all,这避免了循环向量化器再次尝试优化循环。

循环展开

如果存在任何!{!"llvm.loop.unroll.enable"}元数据或选项(llvm.loop.unroll.countllvm.loop.unroll.full),则展开被解释为强制展开。展开可以是完全展开、具有常数循环次数的循环的部分展开或具有编译时未知循环次数的循环的运行时展开。

如果循环已完全展开,则没有后续循环。对于部分/运行时展开,原始循环

for (int i = 0; i < n; i+=1) // original loop
  Stmt(i);

将转换为(使用展开因子 4)

int i = 0;
for (; i + 3 < n; i+=4) { // unrolled loop
  Stmt(i);
  Stmt(i+1);
  Stmt(i+2);
  Stmt(i+3);
}
for (; i < n; i+=1) // remainder loop
  Stmt(i);

llvm.loop.unroll.followup_unrolled将设置展开循环的循环属性。如果未指定,则复制原始循环的属性(不包括llvm.loop.unroll.*属性),并向其添加llvm.loop.unroll.disable

llvm.loop.unroll.followup_remainder定义剩余循环的属性。如果未指定,则剩余循环将没有任何属性。由于完全展开,剩余循环可能不存在,在这种情况下,此属性无效。

llvm.loop.unroll.followup_all中定义的属性将添加到展开的循环和剩余循环中。

为了避免部分展开的循环再次展开,建议将llvm.loop.unroll.disable添加到llvm.loop.unroll.followup_all。如果为生成的循环未指定任何后续属性,则会自动添加它。

展开并合并

展开并合并使用以下转换模型(此处展开因子为 2)。目前,当转换不安全时,它不支持回退版本。

for (int i = 0; i < n; i+=1) { // original outer loop
  Fore(i);
  for (int j = 0; j < m; j+=1) // original inner loop
    SubLoop(i, j);
  Aft(i);
}
int i = 0;
for (; i + 1 < n; i+=2) { // unrolled outer loop
  Fore(i);
  Fore(i+1);
  for (int j = 0; j < m; j+=1) { // unrolled inner loop
    SubLoop(i, j);
    SubLoop(i+1, j);
  }
  Aft(i);
  Aft(i+1);
}
for (; i < n; i+=1) { // remainder outer loop
  Fore(i);
  for (int j = 0; j < m; j+=1) // remainder inner loop
    SubLoop(i, j);
  Aft(i);
}

llvm.loop.unroll_and_jam.followup_outer将设置展开的外层循环的循环属性。如果未指定,则复制原始外层循环的属性(不包括llvm.loop.unroll.*属性),并向其添加llvm.loop.unroll.disable

llvm.loop.unroll_and_jam.followup_inner将设置展开的内层循环的循环属性。如果未指定,则使用原始内层循环的属性不变。

llvm.loop.unroll_and_jam.followup_remainder_outer设置外层剩余循环的循环属性。如果未指定,则它将没有任何属性。由于完全展开,剩余循环可能不存在。

llvm.loop.unroll_and_jam.followup_remainder_inner 设置内部余数循环的循环属性。如果未指定,则它将具有原始内部循环的属性。如果外部余数循环被展开,则内部余数循环可能会出现多次。

llvm.loop.unroll_and_jam.followup_all 中定义的属性将添加到上述所有输出循环中。

为了避免展开的循环再次被展开,建议将 llvm.loop.unroll.disable 添加到 llvm.loop.unroll_and_jam.followup_all 中。它抑制了展开和合并,以及额外的内部循环展开。如果为生成的循环未指定后续属性,则会自动添加。

循环分配

LoopDistribution 传递尝试将循环的可向量化部分与不可向量化部分分离(否则将使整个循环不可向量化)。从概念上讲,它将如下循环转换为

for (int i = 1; i < n; i+=1) { // original loop
  A[i] = i;
  B[i] = 2 + B[i];
  C[i] = 3 + C[i - 1];
}

以下代码

if (rtc) {
  for (int i = 1; i < n; i+=1) // coincident loop
    A[i] = i;
  for (int i = 1; i < n; i+=1) // coincident loop
    B[i] = 2 + B[i];
  for (int i = 1; i < n; i+=1) // sequential loop
    C[i] = 3 + C[i - 1];
} else {
  for (int i = 1; i < n; i+=1) { // fallback loop
    A[i] = i;
    B[i] = 2 + B[i];
    C[i] = 3 + C[i - 1];
  }
}

其中rtc是生成的运行时检查。

llvm.loop.distribute.followup_coincident 设置所有没有循环携带依赖关系(即可向量化循环)的循环的循环属性。可能存在多个这样的循环。如果未定义,则这些循环将继承原始循环的属性。

llvm.loop.distribute.followup_sequential 设置可能存在不安全依赖关系的循环的循环属性。最多应该存在一个这样的循环。如果未定义,则该循环将继承原始循环的属性。

llvm.loop.distribute.followup_fallback 定义回退循环的循环属性,回退循环是原始循环的副本,用于需要循环版本控制时。如果未定义,则回退循环将继承原始循环的所有属性。

llvm.loop.distribute.followup_all 中定义的属性将添加到上述所有输出循环中。

建议将 llvm.loop.disable_nonforced 添加到 llvm.loop.distribute.followup_fallback 中。这避免了回退版本(可能永远不会执行)被进一步优化,这将增加代码大小。

版本控制 LICM

该传递将代码从仅在动态条件适用时才循环不变的循环中提升出来。例如,它将循环转换为

for (int i = 0; i < n; i+=1) // original loop
  A[i] = B[0];

以下代码

if (rtc) {
  auto b = B[0];
  for (int i = 0; i < n; i+=1) // versioned loop
    A[i] = b;
} else {
  for (int i = 0; i < n; i+=1) // unversioned loop
    A[i] = B[0];
}

运行时条件(rtc)检查数组 A 和元素 B[0] 是否不别名。

目前,此转换不支持后续属性。

循环交换

目前,LoopInterchange 传递不使用任何元数据。

转换顺序不明确

如果定义了多个转换,则执行它们的顺序取决于 LLVM 的传递流水线的顺序,该顺序可能会发生变化。默认优化流水线(高于 -O0 的任何内容)具有以下顺序。

使用传统传递管理器时

  • LoopInterchange(如果启用)

  • SimpleLoopUnroll/LoopFullUnroll(仅执行完全展开)

  • VersioningLICM(如果启用)

  • LoopDistribute

  • LoopVectorizer

  • LoopUnrollAndJam(如果启用)

  • LoopUnroll(部分和运行时展开)

使用带有 LTO 的传统传递管理器时

  • LoopInterchange(如果启用)

  • SimpleLoopUnroll/LoopFullUnroll(仅执行完全展开)

  • LoopVectorizer

  • LoopUnroll(部分和运行时展开)

使用新的传递管理器时

  • SimpleLoopUnroll/LoopFullUnroll(仅执行完全展开)

  • LoopDistribute

  • LoopVectorizer

  • LoopUnrollAndJam(如果启用)

  • LoopUnroll(部分和运行时展开)

剩余转换

在最后一个转换传递之后尚未应用的强制转换应报告给用户。转换传递本身无法负责此报告,因为它们可能不在流水线中,可能有多个传递能够应用转换(例如 LoopInterchange 和 Polly),或者转换属性可能“隐藏”在另一个传递的后续属性中。

-transform-warning 传递(WarnMissedTransformationsPass)发出此类警告。它应该放在最后一个转换传递之后。

当前的传递流水线具有一个固定的顺序,其中执行转换传递。转换可以在稍后执行的传递的后续操作中,因此是剩余的。例如,循环嵌套不能在当前传递流水线中分配然后交换。循环分配将执行,但没有后续的循环交换传递,因此任何循环交换元数据都将被忽略。在这种情况下,-transform-warning 应该发出警告。

LLVM 的未来版本可能会通过使用动态排序执行转换来解决此问题。