首页 > 解决方案 > 分支错误预测与缓存未命中

问题描述

考虑以下两个替代代码:

备选方案 1:

if (variable != new_val) // (1)
    variable = new_val;

f(); // This function reads `variable`.

备选方案 2:

variable = new_val; // (2)
f(); // This function reads `variable`.

哪个替代方案“统计上”更快?假设variable在 (1) 或 (2) 之前位于缓存 L1 中。

我猜即使分支误预测率很高,替代方案(1)也会更快,但我真的不知道“ifs”的成本。我的猜测是基于缓存未命中比分支错误预测更昂贵的假设,但我真的不知道。

如果variable在 (1) 或 (2) 之前不在缓存中怎么办?是不是改变了太多局面?

注意:由于不同 CPU 之间的情况可能会发生很大变化,因此您可以根据自己熟悉的架构来回答,尽管首选任何现代 Intel 架构等广泛使用的 CPU。我的问题的目标实际上是更多地了解 CPU 的工作原理。

标签: c++cperformancecpu-architecturebranch-prediction

解决方案


通常,备选方案 2 更快,因为它执行的机器代码更少,并且存储缓冲区会将无条件存储与核心的其他部分分离,即使它们在缓存中丢失。

如果备选方案 1 始终更快,编译器会生成这样做的 asm,但事实并非如此。它引入了可能的分支未命中和可能缓存未命中的负载。在某些看似合理的情况下它可能会更好(例如与其他线程共享错误,或破坏数据依赖性),但这些是您必须通过性能实验和性能计数器确认的特殊情况。


首先读取variable已经触及两个变量的内存(如果两者都不在寄存器中)。如果您希望new_val几乎总是相同(因此它可以很好地预测),并且该负载在缓存中丢失,分支预测+推测执行可能有助于将以后的读取variable与该缓存未命中负载分离。但它仍然是一个需要等待的缓存未命中负载,因为可以检查分支条件,因此如果分支预测错误,总未命中惩罚最终可能会非常大。但是否则,您会通过使更多的后续工作独立于它来隐藏大量缓存未命中负载损失,从而允许 OoO exec 达到ROB size 的限制

除了打破数据依赖性之外,如果内f()联和variable优化到寄存器中,分支将毫无意义。否则,在 L1d 中未命中但在 L2 缓存中命中的存储仍然非常便宜,并且与存储缓冲区的执行脱钩。(推测性执行的 CPU 分支是否可以包含访问 RAM 的操作码?)即使在 L3 中命中对于存储来说也不算太糟糕,除非其他线程具有处于共享状态的行并且弄脏它会干扰它们读取其他全局变量的值。(虚假分享)

请注意,variable即使存储正在等待从存储缓冲区提交到 L1d 缓存(存储转发),以后重新加载也可以使用新存储的值,因此即使f()没有内联并new_value直接使用加载结果,它的使用variable仍然不必等待可能的商店错过variable


避免错误共享是值得进行分支以避免单个存储适合寄存器的值的少数原因之一。

@EOF 在评论中链接的两个问题讨论了这种可能的优化(或可能的悲观化)以避免写入的情况。有时使用std::atomic变量来完成,因为虚假共享是一个更大的问题。mo_seq_cst(并且在除 AArch64 之外的大多数 ISA 上,使用默认内存顺序的存储速度很慢,耗尽了存储缓冲区。)


推荐阅读