简介
条件分支是指后续具有两路可执行的分支。可以分为跳转分支(taken branch)和不跳转分支(not-taken branch)。不跳转分支,指接下来会顺序执行紧挨着JMP的指令。跳转分支,通过JMP跳转到另外一块内存去执行那里的指令。
是否执行条件跳转,只有在该分支指令在流水线中通过了执行阶段才能最终确定下来。
如果没有分支预测技术,处理器将会等待分支指令通过了指令流水线的执行阶段,才把下一条指令送入流水线的第一个阶段--取指阶段。这就会引起流水线停顿或者分支延迟间隙。
分支预测技术会猜测条件表达式里两路分支中哪一路最可能发生,然后推测执行这一路的指令,来避免流水线停顿造成的时间浪费。如果后来发现分支预测错误,那么流水线中推测执行的那些中间结果全部放弃,重新获取正确的分支路线上的指令开始执行,这就导致了程序执行的延迟。
在分支预测失败时浪费的时间是从取指令到执行完指令(但还没有写回结果)的流水线级数。现代微处理器趋向采用非常长的流水线,因此分支预测失败可能会损失10-20个时钟周期。越长的流水线就需要越好的分支预测。
分支预测单元(branch prediction unit)
在上文Intel自顶向下微架构分析方法中介绍了Intel处理器架构的前端和后端,分支预测单元就位于流水线前端之中。分支预测使处理器能够在决定分支结果之前就开始执行指令。 所有分支都会使用 BPU 进行预测。分支预测单元包含以下特性:
- 具有16 通道堆栈返回缓冲区 (RSB),它使 BPU 能够准确预测 RET 指令。
- 前端缓存了BPU中的查找。BPU一次进行32字节的分支预测,是指令获取引擎的2倍。这使得可以再没有惩罚的情况下进行分支预测。
- 尽管BPU的机制是消除跳转分支的惩罚,软件还是应该认为跳转分支(taken branch)所带来的消耗大于不跳转分支(not-taken branch)。
BPU做一下几种预测:
- 直接调用和跳转,目标被当做数组一样读取,不需要考虑跳转或者不跳转的问题。
- 间接调用和跳转,这些可以被预测为具有单调目标或具有根据最近的程序行为而变化的目标。
- 条件分支,预测分支目标和分支是否采取跳转。
分支预测优化
在处理器的架构发展过程当中,硬件以多种方式改进了分支处理。
- 增加了分支目标缓冲区以提高分支预测的准确性。
- 返回堆栈缓冲区支持重命名,以减少代码中返回指令的错误预测。
- 加快资源回收来改进分支预测错误的处理,这样前端就不会在分配资源时等待解码架构代码路径(architected code path,指令将在其中达到退休的代码路径)中的指令或执行错误预测的代码路径中的指令。相反,只要前端开始解码架构代码路径中的指令,新的微操作流就可以开始执行。
分支优化对性能有重大影响。通过了解分支的流程并提高其可预测性,可以显着提高代码速度。Intel优化手册上给出的分支预测优化在软件上可以改进的是:
- 尽可能消除分支。
- 安排代码与静态分支预测算法一致。
- 使用内联函数。
- 根据需要展开循环,以便重复执行的循环具有 16 次或更少的迭代(除非这会导致代码大小过度增加)。
- 避免将两个条件分支指令放入同一循环中。
- 将代码和数据保存在不同的页面上。在极少数情况下,将代码页上的数据作为指令执行可能会导致性能问题。 当执行的指令不在跟踪缓存中的间接分支时,很可能会发生这种情况。
间接分支优化
间接分支或调用的默认预测目标是贯穿路径。如果硬件预测可用于该分支,则贯穿路径预测会被覆盖。分支预测器对间接分支的预测是之前已经执行过的路径。由于代码局部性差或者分支条件冲突而导致无法进行分支预测的情况,那么默认的预测目标就会是贯穿路径。
代码中的switch语句,goto语句或者通过指针的调用可以跳转到代码的任意位置。如果代码序列使得分支的目标大部分时间都能达到同一地址,那么分支目标缓存(branch target buffer)将在大部分时间都准确预测。由于分支目标缓存中只能存储一个目标,因此具有多个分支的间接分支预测率可能较低。
如果一个间接分支有两个或多个通用的目标,并且至少有一个目标与导致该分支的历史相关,则可以将此目标剥离出来,独立于具有多个分支的间接分支之前。此方法的目的是通过增强分支的可预测性(即使以添加更多分支为代价)来减少错误预测的总数。添加的分支必须是可预测的,这样才有意义。
原始代码:
function ()
{
int n = rand(); // 随机数 0 - RAND_MAX
if ( ! (n & 0x01) ) // n 有一半概率置 0
{
n = 0;
}
// 有数个跳转目标的间接分支会降低预测成功的概率
switch (n)
{
case 0: handle_0(); break; // common target
case 1: handle_1(); break; // uncommon
case 3: handle_3(); break; // uncommon
default: handle_other(); // common target
}
}
优化后代码:
function ()
{
int n = rand(); // 随机数 0 - RAND_MAX
if( ! (n & 0x01) ) // n 有一半概率置 0
{
n = 0;
}
if (!n)
{
handle_0(); // 根据相关的分支历史,剥离最通用的分支目标
}
switch (n)
{
case 1: handle_1(); break; // Uncommon
case 3: handle_3(); break; // Uncommon
default: handle_other(); // 使最有可能发生的目标为贯穿路径
}
}
参考文档:
64-ia-32-architectures-optimization-manual
https://zh.wikipedia.org/wiki/分支预测器
CN02812893.1 预测间接分支目标地址的方法、装置和编译器