大多数(如果不是所有的话)现代处理器都使用一种称为"分支预测"的技术,它可以猜测if-then-else分支的走向。
关于这个方案,我有一个问题。假设我们有这样一段代码,没有特定的语言:if(someCondition)
{
// some action
return someValue;
}
// some other action
return someOtherValue;
从逻辑上讲,该代码等价于以下代码:
if(someCondition)
{
// some action
return someValue;
}
else
{
// some other action
return someOtherValue;
}
分支预测器会"预测"第二个例子中的分支,但是第一个例子呢?它会猜吗?什么将被加载到管道中?在忽略实际代码块的影响的情况下,这两个示例中是否有任何速度可以获得?
我猜,这取决于编译器:If语句(在汇编中)使用跳转实现,只有当寄存器中的比较标志设置时才会进行跳转。汇编指令的具体样子取决于编译器。除非有一个共同的方式来处理它,每个编译器都做,我怀疑是否有,然后这是编译器依赖。在这种情况下,最新的Visual Studio c++和gc++编译器会发生什么?
正如十六进制所指出的,返回值之间的关系以及如何确定someCondition
…分支预测器可能不会启动。让我们只考虑true和false作为返回值。对于这个条件,让我们假设它是一个预先确定的字段,在函数内部或外部,一个局部变量和一些算术语句。
老实说,我不认为条件是局部变量的情况与在同一函数中预先确定字段的情况有多大区别。
最有可能的gcc -O3
将使用条件移动指令将其优化为无分支序列。例如在x86上
# generate someValue in %rax, the x86-64 ABI's return value register
# generate someOtherValue in %rdi, to pick one at random
test someCondition # probably actually test or cmp a register
cmovz %rdi, %rax # copy %rdi to %rax, if the zero flag is set.
ret
cmov对其输入和标志都有数据依赖。条件分支是一个控件依赖项。使用cmov通常是好的,除非它是一个长依赖链的一部分,并且分支是相当可预测的。
如果if
块中有更多的工作,gcc将生成一个条件跳转指令。
# generate someValue in %rax
test someCondition
jz .zero
ret
.zero:
# compute someOtherValue. This work doesn't need to happen at all
# if we don't end up needing it, unlike in the cmov case
mov someOtherValue, %rax
ret
分支预测操作条件跳转指令,而不是高级结构。如果循环条件为真,则使用相同的指令跳转回循环的顶部。根据http://agner.org/optimize/的说法,最近的英特尔cpu可以记住多达64次循环迭代的模式。因此,每次运行相同迭代次数的循环在最后一次迭代中不会有分支错误预测,如果迭代次数为64或更少。
所以分支预测器并不是根据指令序列来猜测跳转是否会发生。每个单独的分支指令在执行时都会在分支历史缓冲区中获得一个条目。是的,每个编译器都别无选择,只能使用jcc
(Jump on Condition Code)指令来实现分支/循环。
默认值为"未取"。如果这个预测是正确的,CPU不会从缓存中驱逐可能仍然有用的信息来腾出空间。查看Agner Fog的microarch文档了解更多底层细节。
在Linux上,要查看分支预测器的运行情况,可以使用perf stat
:
perf stat /bin/ls # in some big directory
... normal ls output
Performance counter stats for '/bin/ls':
10.403069 task-clock (msec) # 0.094 CPUs utilized
2,255 context-switches # 0.217 M/sec
0 cpu-migrations # 0.000 K/sec
190 page-faults # 0.018 M/sec
16,612,260 cycles # 1.597 GHz
7,843,399 stalled-cycles-frontend # 47.21% frontend cycles idle
5,205,565 stalled-cycles-backend # 31.34% backend cycles idle
20,227,093 instructions # 1.22 insns per cycle
# 0.39 stalled cycles per insn
3,975,777 branches # 382.173 M/sec
########### These two lines ######
55,785 branch-misses # 1.40% of all branches
0.110765717 seconds time elapsed
Intel Sandybridge (i5 2500k),在低功耗时钟速度下,使用默认的cpufreq调控器,在ls
完成之前不会增加时钟速度。
这两个代码示例之间没有区别。else
是不相关的,因为不需要在true子句的末尾进行分支。即使这不是真的,true子句末尾的分支也不是条件的。
换句话说,代码必须编译成如下格式:
Compute test expression
Branch if false to false_label
True action
Return some value
False_label;
False action
Return some other value