程式碼轉換中繼資料

概述

LLVM 轉換過程可以通過將中繼資料附加到要轉換的程式碼來控制。默認情況下,轉換過程使用啟發式方法來確定是否執行轉換,以及在執行轉換時,如何應用轉換的其他細節(例如,選擇哪個向量化因子)。除非優化器另有指示,否則轉換將保守地應用。這種保守性通常允許優化器避免無利可圖的轉換,但在實踐中,這會導致優化器不應用會非常有利可圖的轉換。

前端可以向 LLVM pass 提供關於它們應該應用哪些轉換的額外提示。這可以是無法從發出的 IR 中推導出的額外知識,或者從使用者/程式設計師傳遞的指令。OpenMP pragma 就是後者的一個例子。

如果從程式中刪除了任何此類中繼資料,則程式碼的語義不得更改。

迴圈上的中繼資料

可以將屬性附加到迴圈,如 ‘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)可以被優化器忽略而不產生警告。

除非明確禁用,否則任何優化過程都可以啟發式地確定轉換是否有益並應用它。如果指定了另一個轉換的元數據,則在其之前應用不同的轉換可能會由於應用在不同的循環上或循環不再存在而產生意外結果。為了避免必須明確禁用未知數量的過程,屬性 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"}}

當且僅當未指定後續操作時,過程本身可能會添加屬性。例如,向量化器會添加 llvm.loop.isvectorized 屬性以及原始循環的所有屬性(不包括其循環向量化器屬性)。為避免這種情況,可以使用空的後續屬性,例如:

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

無法應用的轉換的後續屬性永遠不會添加到循環中,因此實際上會被忽略。這表示此類屬性中的任何後續轉換都需要在其之前的轉換應用於後續轉換之前應用。如果強制轉換鏈中的第一個轉換無法應用,則用戶應收到有關該轉換的警告。所有後續轉換都將被跳過。

特定於過程的轉換元數據

轉換選項特定於每個轉換。在下文中,我們將介紹每個 LLVM 循環優化過程的模型以及影響它們的元數據。

循環向量化和交錯

循環向量化和交錯被解釋為單一轉換。如果設置了 !{"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。它會抑制展開和合併,以及額外的內部迴圈展開。如果沒有為生成的迴圈指定後續屬性,則會自動添加。

迴圈分配

迴圈分配遍歷嘗試將迴圈中可向量化的部分與不可向量化的部分分開(否則會導致整個迴圈不可向量化)。從概念上講,它會將如下所示的迴圈

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可能會透過使用動態排序執行轉換來解決此問題。