LLVM 分支权重中继数据

简介

分支权重中继数据将其被执行的可能性表示为分支权重(请参阅 LLVM 块频率术语)。中继数据作为 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

呼叫指令可以包含帶有一個或兩個權重的分支權重中繼資料。第二個權重是可選的,對應於展開分支。如果只設定了一個權重,則它包含呼叫的執行次數,並且僅在 SamplePGO 模式下使用,如呼叫指令中所述。如果指定了兩個權重,則第二個權重包含已執行的展開分支計數,第一個權重包含呼叫的執行次數減去已執行的展開分支計數。指定的兩個權重都用於計算 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(真),則條件很可能是真,否則條件很可能是假。例如

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

switch 語句

exp 參數是值。c 參數是預期值。如果預期值未出現在 cases 清單中,則假設 default 案例很可能會被採用。

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(真),並且機率值 probability 設定為 0.8,則表示條件為真的機率為 80%,而為假的機率為 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” 有兩個以上的操作數,則後面的操作數是 ThinLTO 需要導入的函數的 GUID。這僅由基於採樣的配置文件設置。這是必需的,因為基於採樣的配置文件是在已經導入和內聯這些函數的二進制文件上收集的,並且我們需要確保 ThinLTO 後端中的 IR 與配置文件註釋相匹配。我們無法在調用站點上對其進行註釋的原因是,它在調用鏈中只能下降 1 級。對於 foo_in_a_cc()->bar_in_b_cc()->baz_in_c_cc() 的情況,我們需要在調用鏈中下降 2 級才能導入 bar_in_b_cc 和 baz_in_c_cc。