程式碼轉換元數據¶
總覽¶
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.enable
和 llvm.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.isvectorized
和 llvm.loop.unroll.runtime.disable
(除非原始迴圈已經有展開元數據)。
llvm.loop.vectorize.followup_all
指定的屬性將新增到兩個迴圈。
當使用後續屬性時,它會替換為所生成的迴圈自動推導出的任何屬性。因此,建議將 llvm.loop.isvectorized
新增到 llvm.loop.vectorize.followup_all
,這避免了迴圈向量化器嘗試再次優化迴圈。
迴圈展開¶
展開被解釋為強制執行,任何 !{!"llvm.loop.unroll.enable"}
元數據或選項(llvm.loop.unroll.count
、llvm.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 pass 嘗試將迴圈的可向量化部分與不可向量化部分分開(否則會使整個迴圈不可向量化)。概念上,它將迴圈轉換為如下程式碼
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¶
該 pass 將代碼從迴圈中提升出來,這些代碼僅在應用動態條件時才是迴圈不變的。例如,它轉換迴圈
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
pass 不使用任何元數據。
模糊的轉換順序¶
如果定義了多個轉換,它們的執行順序取決於 LLVM 的 pass 管線中的順序,這可能會更改。預設優化管線(高於 -O0
的任何級別)具有以下順序。
當使用舊版 pass 管理器時
LoopInterchange (如果啟用)
SimpleLoopUnroll/LoopFullUnroll (僅執行完全展開)
VersioningLICM (如果啟用)
LoopDistribute
LoopVectorizer
LoopUnrollAndJam (如果啟用)
LoopUnroll (部分和運行時展開)
當使用具有 LTO 的舊版 pass 管理器時
LoopInterchange (如果啟用)
SimpleLoopUnroll/LoopFullUnroll (僅執行完全展開)
LoopVectorizer
LoopUnroll (部分和運行時展開)
當使用新版 pass 管理器時
SimpleLoopUnroll/LoopFullUnroll (僅執行完全展開)
LoopDistribute
LoopVectorizer
LoopUnrollAndJam (如果啟用)
LoopUnroll (部分和運行時展開)
剩餘的轉換¶
在最後一個轉換 pass 之後尚未應用的強制轉換應向使用者報告。轉換 pass 本身不能負責此報告,因為它們可能不在管線中,可能有多個 pass 能夠應用轉換(例如 LoopInterchange
和 Polly),或者轉換屬性可能「隱藏」在另一個 pass 的後續屬性內。
pass -transform-warning
(WarnMissedTransformationsPass
) 發出此類警告。它應放置在最後一個轉換 pass 之後。
當前 pass 管線具有固定的轉換 pass 執行順序。轉換可能在稍後執行的 pass 的後續中,因此會剩餘。例如,迴圈巢狀結構無法分佈然後與當前 pass 管線交換。迴圈分佈將執行,但沒有後續的迴圈交換 pass,因此任何迴圈交換元數據都將被忽略。-transform-warning
在這種情況下應發出警告。
LLVM 的未來版本可能會通過使用動態排序執行轉換來修正這個問題。