我看到有人说完美预测的分支几乎没有开销。(例如在分支预测对性能的影响?的回答中)
我不太明白人们是什么意思。至少分支条件必须被评估,这可能是一个简单的布尔值或一个函数调用,需要时间。
我看到有人说完美预测的分支几乎没有开销。(例如在分支预测对性能的影响?的回答中)
我不太明白人们是什么意思。至少分支条件必须被评估,这可能是一个简单的布尔值或一个函数调用,需要时间。
即使是完美预测,评估分支条件也总是需要一些工作的,但由于现代CPU内部的并行性,额外的工作并不一定会增加特定指令序列的成本。
我认为部分混淆是因为许多人对CPU指令执行的心理表现模型。是的,每个指令都需要一些工作,这应该意味着每个指令在执行时间上都有一些成本,即使很小,对吗?
如果执行的总成本只是每个指令的工作量相加,那么这就是真的了-您只需将所有工作量相加即可获得最终成本。但由于现代CPU中的大量并行处理,它并不像那样工作。
想象一下组织生日派对的情景。你可能需要购买面粉,需要10分钟,然后烤蛋糕需要60分钟,再去拿一个特别的礼物需要30分钟。这些时间都是活动所需的“工作”。但是,当面粉被取走和蛋糕正在被烤制时,有人可以去拿礼物。然而,没有面粉就无法烤蛋糕。因此,你有两个依赖链:70分钟的购买面粉->烤蛋糕链和30分钟的拿礼物链。在无限并行的情况下,只有70分钟与蛋糕相关的链条对所有任务完成的时间做出贡献。拿礼物需要30分钟的“工作”,但由于其他需要更长时间(即关键路径)并且同时进行的工作,它不会耗费时间(不会延迟所有任务的完成)。如果您想更加具体地了解这些内容如何直接应用于CPU,请参阅数据流图的风暴式介绍。
一旦我们拥有了这种新的心理模型,其中指令序列的成本往往由某个关键路径主导,我们就可以开始看到为什么预测良好的分支通常非常低廉甚至没有成本:
这些因素结合起来使得大多数预测的分支指令成本为零或几乎为零。
你不必相信我的话,让我们看一个真实的例子:
int mul1(int count, int x) {
do {
x *= 111;
} while (--count);
return x;
}
给定一个计数器 count
和一个起始值 x
,它将 x
乘以 111 count
次并返回结果。循环组装三条指令:一条用于乘法,一条用于 --count
,另一条用于检查 count
的值:
.L2:
imul eax, eax, 111
sub edi, 1
jne .L2
int mul2(int count, int x) {
do {
x *= 111;
if (x == 0) {
abort();
}
} while (--count);
return x;
}
这组装成了5条指令。多余的两条是用于测试x
和分支,测试显示x
为零:
.L7:
imul eax, eax, 111
test eax, eax
je .L12 ; ends up calling abort
sub edi, 1
jne .L7
那么增加60%的指令成本是多少,包括分支?至少到4个有效数字为止,成本为零3。
Running benchmarks groups using timer libpfc
** Running benchmark group stackoverflow tests **
Benchmark Cycles
No branch 3.000
Added test-branch 3.000
1 从概念上讲,分支指令会写入“rip”寄存器,但它与其他寄存器的处理方式不同: 它的进展是预测的,因此预测器会打破依赖。
2 当然,要解码和融合指令仍需要额外工作,但这通常不是瓶颈,因此在成本方面可能是“免费”的,而像uop缓存这样的东西意味着它甚至可能不经常执行。此外,在x86上,虽然融合的分支指令具有与ALU操作相同的延迟,但在可执行的端口方面却不太灵活,因此根据端口压力的情况,与裸露的ALU操作相比,一条融合指令可能具有一些成本。
3 实际上,如果你去看“无限”有效数字并查看原始周期计数,你会发现这个循环的额外迭代在两种情况下都需要恰好 3个周期。无分支的情况通常会以整体较短的1个周期结束(随着迭代次数的增加,这个差异相对于整体趋近于0),可能是因为初始的非稳定状态迭代需要额外一个周期,或者误预测恢复在最后一次迭代上需要额外一个周期。
现代大多数CPU还支持“预测执行”。这意味着处理器会在实际确定条件之前开始执行指令。因此,处理器将在老板说之前从A车转换到B车上进行工作。如果此时老板说“不,你应该继续处理A车”,那么你就必须放弃一堆已经开始工作的汽车。你不一定要建造所有的汽车,但是你必须按照每20分钟一个步骤的生产线移动它们。