LLVM 分支權重元數據

簡介

分支權重元數據將分支權重表示為其被採用的可能性 (請參閱 LLVM 區塊頻率術語)。元數據被指派給作為終結符的 Instruction,作為 MD_prof 類型的 MDNode。第一個運算元始終是一個帶有字串 “branch_weights” 的 MDString 節點。運算元的數量取決於終結符類型。

分支權重可以從分析檔案中獲取,或者基於 __builtin_expect__builtin_expect_with_probability 指令生成。

所有權重都表示為無符號 32 位元值,其中較高的值表示被採用的機會更大。

支援的指令

BranchInst

元數據僅指派給條件分支。對於 true 和 false 分支,有兩個額外的運算元。我們可以選擇性地追蹤元數據是否由 __builtin_expect__builtin_expect_with_probability 添加,並帶有一個可選欄位 !"expected"

!0 = !{
  !"branch_weights",
  [ !"expected", ]
  i32 <TRUE_BRANCH_WEIGHT>,
  i32 <FALSE_BRANCH_WEIGHT>
}

SwitchInst

分支權重被指派給每個 case(包括始終為 case #0 的 default case)。

!0 = !{
  !"branch_weights",
  [ !"expected", ]
  i32 <DEFAULT_BRANCH_WEIGHT>
  [ , i32 <CASE_BRANCH_WEIGHT> ... ]
}

IndirectBrInst

分支權重被指派給每個目的地。

!0 = !{
  !"branch_weights",
  [ !"expected", ]
  i32 <LABEL_BRANCH_WEIGHT>
  [ , i32 <LABEL_BRANCH_WEIGHT> ... ]
}

CallInst

呼叫可能具有分支權重元數據,其中包含呼叫的執行計數。它目前僅在 SamplePGO 模式中使用,以擴增區塊和入口計數,這些計數在取樣時可能不準確。

!0 = !{
  !"branch_weights",
  [ !"expected", ]
  i32 <CALL_BRANCH_WEIGHT>
}

InvokeInst

Invoke 指令可能具有帶有一個或兩個權重的分支權重元數據。第二個權重是可選的,對應於 unwind 分支。如果僅設定一個權重,則它包含呼叫的執行計數,並且僅在 SamplePGO 模式中使用,如呼叫指令所述。如果指定了兩個權重,則第二個權重包含 unwind 分支被採用的計數,而第一個權重包含呼叫的執行計數減去 unwind 分支被採用的計數。指定的兩個權重都用於計算 BranchProbability,如同 BranchInst 一樣,對於 SamplePGO,則使用兩個權重的總和。

!0 = !{
  !"branch_weights",
  [ !"expected", ]
  i32 <INVOKE_NORMAL_WEIGHT>
  [ , i32 <INVOKE_UNWIND_WEIGHT> ]
}

其他

不允許其他終結符指令包含分支權重元數據。

內建 expect 指令

__builtin_expect(long exp, long c) 指令提供分支預測資訊。傳回值是 exp 的值。

它在條件語句中特別有用。目前 Clang 支援兩種條件語句

if 語句

exp 參數是條件。 c 參數是預期的比較值。如果它等於 1 (true),則條件很可能為 true;在其他情況下,條件很可能為 false。例如

if (__builtin_expect(x > 0, 1)) {
  // This block is likely to be taken.
}

switch 語句

exp 參數是值。 c 參數是預期的值。如果預期的值未在 case 列表中顯示,則假定 default case 很可能被採用。

switch (__builtin_expect(x, 5)) {
default: break;
case 0:  // ...
case 3:  // ...
case 5:  // This case is likely to be taken.
}

內建 expect.with.probability 指令

__builtin_expect_with_probability(long exp, long c, double probability) 具有與 __builtin_expect 相同的語意,但呼叫者提供 exp == c 的機率。最後一個引數 probability 必須是常數浮點表達式,並且在 [0.0, 1.0] 的範圍內(含)。用法也與 __builtin_expect 類似,例如

if 語句

如果預期的比較值 c 等於 1 (true),且機率值 probability 設定為 0.8,則表示條件為 true 的機率為 80%,而為 false 的機率為 20%。

if (__builtin_expect_with_probability(x > 0, 1, 0.8)) {
  // This block is likely to be taken with probability 80%.
}

switch 語句

這基本上與 __builtin_expect 中的 switch 語句相同。 exp 等於預期值的機率在第三個引數 probability 中給出,而其他值的機率是剩餘機率(1.0 - probability)的平均值。例如

switch (__builtin_expect_with_probability(x, 5, 0.7)) {
default: break;  // Take this case with probability 10%
case 0:  break;  // Take this case with probability 10%
case 3:  break;  // Take this case with probability 10%
case 5:  break;  // This case is likely to be taken with probability 70%
}

CFG 修改

分支權重元數據無法防止 CFG 變更。如果終結符運算元被更改,則應採取一些措施。在其他情況下,由於不正確的分支預測資訊,可能會發生一些錯誤最佳化。

函數入口計數

為了允許在程序間分析和最佳化期間比較不同的函數,MD_prof 節點也可以指派給函數定義。第一個運算元是一個字串,指示相關計數器的名稱。

目前,支援一個計數器:“function_entry_count”。第二個運算元是一個 64 位元計數器,指示此函數被調用的次數(在基於檢測的分析檔的情況下)。在基於取樣的分析檔的情況下,此運算元是函數被調用次數的近似值。

例如,在下面的程式碼中,函數 foo() 的檢測表明它在運行時被調用了 2,590 次。

define i32 @foo() !prof !1 {
  ret i32 0
}
!1 = !{!"function_entry_count", i64 2590}

如果 “function_entry_count” 具有超過 2 個運算元,則後面的運算元是 ThinLTO 需要匯入的函數的 GUID。這僅由基於取樣的分析檔設定。這是必要的,因為基於取樣的分析檔是在已匯入並內聯這些函數的二進制檔上收集的,並且我們需要確保 IR 在 ThinLTO 後端中與分析檔註解相符。我們無法在呼叫點上註解此內容的原因是它在呼叫鏈中只能下降 1 個層級。對於 foo_in_a_cc()->bar_in_b_cc()->baz_in_c_cc() 的情況,我們將需要在呼叫鏈中下降 2 個層級才能匯入 bar_in_b_cc 和 baz_in_c_cc。