关于c ++:分支预测和除零

Branch Prediction and Division By Zero

我写的代码看起来像是…

1
2
3
4
if(denominator == 0){
    return false;
}
int result = value / denominator;

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

https://stackoverflow.com/a/11227902/620863此答案表示CPU将尝试正确猜测分支的运行方向,如果发现分支的猜测不正确,则向下移动该分支以停止。

但是,如果CPU错误地预测上面的分支,它将在下面的指令中除以零。但这并没有发生,我想知道为什么?CPU真的执行一个除数为零的除法,并在执行任何操作之前等待看看分支是否正确,或者它能告诉我们在这些情况下它不应该继续吗?发生什么事?


在基于预测的推测性地执行分支时,CPU可以自由地做它想要做的任何事情。但它需要以一种对用户透明的方式来实现。因此,它可能会形成一个"被零除"的断层,但如果分支预测结果是错误的,那么这应该是不可见的。按照同样的逻辑,它可以将写操作阶段性地转移到内存中,但实际上可能不会提交它们。

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

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


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

  • 机器代码中有一个测试。
  • 处理器在其中一个可能的路径上加载带有指令的IT管道,并可能在内部执行它们-根据msalters,一些处理器甚至可以执行这两个路径(*)
  • 如果这是一个很好的猜测,好吧,下面的指令已经预加载到处理器缓存中或者已经执行,并且所有的操作都尽可能快。
  • 如果猜测错误,只需清理所有内容并重新启动正确的分支。

与参考站类似,如果道岔位置不正确,列车必须立即停在交叉口,不能在错误的路径上进入下一站,如果在此之前不能停车,则任何乘客不得进出列车。

(*)Itanium处理器可以并行处理多条路径。英特尔的逻辑是,他们可以构建宽处理器(并行工作很多),但他们正在努力提高有效指令率。通过投机地执行这两个分支,他们使用了大量的硬件(我认为他们可以实现多个层次的深度,运行2^n个分支),但它确实有助于明显的单核速度,因为它实际上总是预测一个硬件单元中的正确分支-为了达到这个精度,学分应该转到MSChanges。


除以零没有什么特别的。这是一个由ALU处理的条件,以产生一些效果,例如为商指定一个特殊值。如果启用了此异常类型,它还可以引发异常。

与代码段比较

1
2
3
4
if (denominator == 0) {
    return false;
}
int result = value * denominator;

乘法可以被推测地执行,然后在不知道的情况下取消。对于一个部门也是如此。别担心。


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?

这很可能发生,但问题是:它是可观测的吗?显然,这种由零进行的推测性除法不会也不应该"崩溃"CPU,但对于非推测性的由零进行除法,这种情况甚至不会发生。在除数为零和您的进程退出时出现错误消息之间有一个长的因果链。有点像这样(在posix、x86上):

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

与一个简单的、无错误的除法相比,这是一项大量的工作,而且许多工作都可以通过推测来执行。基本上,在实际的mmap'ed I/O之前,或者在有限的推测性执行资源集(例如卷影寄存器和临时缓存线)用完之前,任何事情都不会发生。后者很可能发生得更快。在这种情况下,需要挂起推测性分支,直到明确是否实际执行并提交更改为止(一旦写入更改,则可以释放推测性执行资源),或者是否应该放弃更改。

重要的一点是:只要其他线程、同一线程上的其他推测分支或其他硬件(如图形)看不到任何推测性执行状态,就可以进行任何优化。然而,现实地说,MSChanges绝对正确,因为CPU设计者不愿意为此用例进行优化。因此,我同样认为,一旦设置了错误标志,一个真正的CPU可能只会挂起推测性分支。如果错误是合法的,那么这最多要花费几个周期,甚至这是不可能的,因为您描述的模式是常见的。通过这一点进行推测性的执行只会从更重要的案例中转移宝贵的优化资源。

(事实上,如果我是一个CPU设计人员,我唯一想要做的处理器异常是一种特定类型的页面错误,在这种错误中页面是已知的和可访问的,但是"present"标志被清除,这是因为当使用虚拟内存时通常会发生这种情况,而不是一个真正的错误。不过,即使是这种情况也不太重要,因为交换时的磁盘访问,甚至只是内存解压,通常都要昂贵得多。)