性能分析 | 分支预测
基础知识
基本块
我们如何确定程序控制流?
我们基本上忽略基本块中的其他指令,因为分支总是基本块中的最后一个指令。
由于基本块中的所有指令都保证执行一次,因此我们只能关注将“代表”整个基本块的分支。因此,如果我们跟踪每个分支的结果,就可以重建程序的整个逐行执行路径。
在编译原理中,基本块是指一段连续的指令序列,它有一个单一入口(即从基本块的第一条指令进入)和单一出口(即基本块中的最后一条指令通常是一个分支指令或者直接跳转)。在一个基本块内部,所有指令都会被顺序执行,不会中途跳转或中断。
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 的函数调用,等等。