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(包括 default case,它始终是 case #0)。

!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,对于 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](包括 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”具有两个以上操作数,则后面的操作数是需要由 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。