分支预测和除以零

Branch Prediction and Division By Zero

我正在编写如下所示的代码...

if(denominator == 0){
    return false;
}
int result = value / denominator;

... 当我想到 CPU 中的分支行为时。

这个答案说 CPU 会尝试正确猜测一个分支会走哪条路,如果它发现它猜错了分支,就会继续沿着那个分支走下去。

但是如果CPU错误地预测了上面的分支,它会在下面的指令中除以零。但这并没有发生,我想知道为什么? CPU 是否实际执行除以零并在执行任何操作之前等待查看分支是否正确,或者它是否可以告诉它在这些情况下不应该继续?怎么回事?

不完全是。系统不允许在错误的分支中执行指令,即使它猜错了,或者更确切地说,如果它猜错了,它一定是不可见的。基本是:

  • 机器代码中某处有一个测试。
  • 处理器在其中一条可能的路径上加载指令,并可能在内部执行它们 - 根据 MSalters 的说法,某些处理器甚至可以执行两者 路径 (*)
  • 如果它猜对了,很好,下面的指令已经预加载到处理器缓存中或已经执行,一切都尽可能快
  • 如果它猜错了,它只需要清除所有内容并在正确的分支上重新启动。

类比post,如果开关位置不正确,火车必须立即停在路口,不能走错路去下一站,或者不能在此之前停止,禁止乘客上下车

(*) Itanium 处理器将能够并行处理许多路径。英特尔的逻辑是,他们可以构建宽处理器(可以并行执行大量操作),但他们在有效指令率方面苦苦挣扎。通过推测性地执行两个分支,他们使用了很多硬件(我认为他们可以深入几层,运行 2^N 个分支)但它确实有助于明显的单核速度,因为它实际上总是预测正确一个 HW 单元中的分支 - 学分应该用于该精度的 MSalters

CPU 在基于预测推测性地执行分支时可以自由地做任何它想做的事情。但它需要以对用户透明的方式进行。所以它可能会出现 "division by zero" 故障,但如果分支预测结果是错误的,这应该是不可见的。按照同样的逻辑,它可能会暂存写入内存,但实际上可能不会提交它们。

作为一名 CPU 设计师,我不会费心去预测过去这样的错误。那可能不值得。故障可能意味着预测错误,很快就会自行解决。

这种自由是好事。考虑一个简单的 std::accumulate 循环。分支预测器将正确预测很多跳转(for (auto current = begin, current != end; ++current),通常跳回到循环的开始),并且有很多内存读取可能有潜在的错误(sum += *current)。但是 CPU 在上一个分支被解析之前拒绝读取内存值会慢很多。然而,在循环结束时错误预测的跳转很可能会导致无害的内存错误,因为预测的分支试图读取缓冲区。这需要在没有明显错误的情况下解决。

But if the CPU predicts the branch above incorrectly, it would divide by zero in the following instructions. This doesn't happen though, and I was wondering why?

这很可能发生,但问题是:它是可观察的吗?显然,这种推测性的除以零不会也不应该 "crash" 而 CPU,但是对于非推测性的除以零甚至不会发生这种情况。除以零与您的进程退出并显示错误消息之间存在很长的因果链。它有点像这样(在 POSIX、x86 上):

  • 负责除法的 ALU 或微代码将被零除标记为错误。
  • 中断描述符 #0 已加载(int 0 表示在 x86 上除以零错误)。
  • 一组寄存器(包括当前程序计数器)被压入堆栈。可能需要先从RAM中获取相应的缓存行。
  • 中断处理程序被执行(一段内核代码)。它在当前进程中引发 SIGFPE 信号。
  • 最终,信号处理决定采取默认操作(假设您没有安装处理程序),即显示错误消息并终止进程。
  • 这需要许多额外的步骤(例如使用设备驱动程序),直到最终用户可以观察到变化,即内存映射输出的一些图形 I/O。

与简单、无差错的除法相比,这是很多工作,其中很多可以推测性地执行。基本上任何事情,直到实际的 mmap'ed I/O,或者直到用于推测执行的有限资源集(例如影子寄存器和临时缓存行)耗尽。后者可能会发生得更快、更早。在这种情况下,需要暂停推测分支,直到明确是否真正采取了更改并应提交更改(更改一旦写入,即可释放推测执行资源),或者是否应该更改更改丢弃。

重要的一点是:只要推测执行状态的 none 对其他线程、同一线程上的其他推测分支或其他硬件(例如图形)可见,为了优化什么都行。然而,实际上,MSalters 是绝对正确的,CPU 设计人员不会关心针对此用例进行优化。所以我同样认为,一旦设置了错误标志,真正的 CPU 可能只会暂停推测分支。这最多花费几个周期 如果错误甚至是合法的 ,即使这样也不太可能,因为您描述的模式很常见。在这一点之后进行推测性执行只会从更重要的案例中转移宝贵的优化资源。

(事实上,如果我是一名 CPU 设计师,我唯一想做的相当快的处理器异常是一种特定类型的页面错误,其中页面是已知且可访问的,但是 "present" flag is cleared, just because this happen commonly when virtual memory is used, and is not a true error. 但即使这种情况也不是很重要,因为交换时的磁盘访问,甚至只是内存解压缩,是通常要贵得多。)

除以零没什么特别的。这是一种由 ALU 处理以产生某些效果的条件,例如为商分配一个特殊值。如果已启用此异常类型,它也可以引发异常。

与代码段比较

if (denominator == 0) {
    return false;
}
int result = value * denominator;

可以推测性地执行乘法,然后在您不知情的情况下取消。一个师也一样。别担心。