提问者:小点点

分支错误预测与缓存未命中[关闭]


想改进这个问题吗?更新问题,以便通过编辑这篇文章用事实和引用来回答。

考虑以下两个替代代码:

备选案文1:

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

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

备选案文2:

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

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

我想即使分支错误预测率很高,备选方案(1)也更快,但我真的不知道“如果”的成本。我的猜测是基于缓存未命中比分支错误预测贵得多的假设,但我真的不知道。

如果变量在(1)或(2)之前不在缓存中怎么办?它是否改变了情况太多?

注意:由于不同CPU之间的情况可能会发生很大变化,您可以根据您熟悉的架构来回答,尽管像任何现代英特尔架构一样广泛使用的CPU是首选。我的问题的目标实际上是了解更多关于CPU如何工作的信息。


共1个答案

匿名用户

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

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

首先读取变量已经触及了两个变量的内存(如果两个变量都不在寄存器中)。如果您期望new_val几乎总是相同的(所以它预测得很好),并且对于缓存中未命中的负载,分支预测推测执行有助于将变量的后续读取与缓存未命中负载分离。但是它仍然是一个必须等待的缓存未命中负载,因为可以检查分支条件,所以如果分支预测错误,总的未命中惩罚可能会很大。但除此之外,您通过使更多的后续工作独立于它来隐藏大量的缓存未命中加载惩罚,从而允许OoO exec达到ROB大小的限制。

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

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

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

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

  • 奇怪的优化?在'libuv'中。请解释
  • C优化:条件存储以避免弄脏缓存行