性能分析 | 分支预测

image

from pixiv

基础知识

参考课本

基本块

我们如何确定程序控制流?

我们基本上忽略基本块中的其他指令,因为分支总是基本块中的最后一个指令。

由于基本块中的所有指令都保证执行一次,因此我们只能关注将“代表”整个基本块的分支。因此,如果我们跟踪每个分支的结果,就可以重建程序的整个逐行执行路径。

在编译原理中,基本块是指一段连续的指令序列,它有一个单一入口(即从基本块的第一条指令进入)和单一出口(即基本块中的最后一条指令通常是一个分支指令或者直接跳转)。在一个基本块内部,所有指令都会被顺序执行,不会中途跳转或中断。

if (x > 0) {
    y = 1;
} else {
    y = -1;
}
z = y + 2;

编译器可能会将其划分成多个基本块:

  • 基本块 1:计算 x > 0 的结果,并根据结果跳转到基本块 2 或基本块 3。
    (该块只负责计算条件和执行分支操作)
  • 基本块 2:执行 y = 1; 并跳转到基本块 4。
  • 基本块 3:执行 y = -1; 并跳转到基本块 4。
  • 基本块 4:执行 z = y + 2;。

在这里,每个基本块内的所有指令都会被“整体”执行一次。因为分支指令总是在基本块的末尾,所以我们通常只关注基本块末尾的那个分支指令,来了解程序的控制流走向。

LBR

硬件并行记录每个分支的“来自”和“到”地址以及一些额外数据,同时执行程序。如果我们收集足够长的源目的地对历史记录,我们将能够像调用堆栈一样解开程序的控制流,但深度有限。

英特尔首次在其 Netburst 微架构中实现了其最后分支记录 (LBR) 功能。LBR 寄存器就像一个不断被覆盖的环形缓冲区,仅提供最近的 32 个分支结果。每个 LBR 条目由三个 64 位值组成:

  • 分支的源地址(“来自 IP”)。
  • 分支的目标地址(“到 IP”)。
  • 操作的元数据,包括错误预测和经过周期时间信息。

当采样计数器溢出并触发性能监控中断 (PMI) 时,LBR 记录冻结,直到软件捕获 LBR 记录并恢复收集。

分支包括循环后缘 JNE、条件分支 JNS 、函数 CALL 和从此函数返回RET等,LBR 数组的深度是有限的,其中执行流的转换伴随着大量叶函数调用。这些对叶函数的调用及其返回很可能会将主执行上下文从 LBR 中移除。

具体移除的方法为:LBR 启用调用堆栈模式。使用这种配置,LBR 数组模拟一个调用堆栈,其中 CALL 会将条目“压入”堆栈,而 RET 则会将条目“弹出”堆栈。因此,与已完成叶函数相关的分支信息将不会保留,同时保留主执行路径的调用堆栈信息。

叶函数(Leaf Function) 是指那些在其内部不调用任何其他函数的函数。它们位于调用树的“叶子”位置,因此被称为叶函数。例如,一个只执行计算或返回结果、没有进一步函数调用的函数,就可以称为叶函数。

使用Linux的perf,可以使用以下命令收集LBR堆栈:

# -b, --branch-any
           Enable taken branch stack sampling. Any type of taken branch may be sampled. This is a shortcut for --branch-filter
           any. See --branch-filter for more infos.
$ perf record -b -e cycles ./benchmark.exe

当启用-b后,我们可以用perf查看到哪些分支被采取的频率最高:

$ perf record -e cycles -b -- ./a.exe
[ perf record: Woken up 3 times to write data ]
[ perf record: Captured and wrote 0.535 MB perf.data (670 samples) ]
$ perf report -n --sort overhead,srcline_from,srcline_to -F +dso,symbol_from,symbol_to --stdio
# Samples: 21K of event 'cycles'
# Event count (approx.): 21440
# Overhead  Samples  Object  Source Sym  Target Sym  From Line  To Line
# ........  .......  ......  ..........  ..........  .........  .......
  51.65%      11074   a.exe   [.] bar    [.] bar      a.c:4      a.c:5
  22.30%       4782   a.exe   [.] foo    [.] bar      a.c:10     (null)
  21.89%       4693   a.exe   [.] foo    [.] zoo      a.c:11     (null)
   4.03%        863   a.exe   [.] main   [.] foo      a.c:21     (null)

从这个例子中,我们可以看到超过 50% 的已采取分支位于 bar 函数内,22% 的分支是来自 foo 到 bar 的函数调用,等等。

具体分析教程

posted @ 2025-04-06 16:17  次林梦叶  阅读(41)  评论(0)    收藏  举报