6

好的,所以我知道如果一个特定的条件分支有一个需要时间来计算的条件(例如内存访问),CPU 会假设一个条件结果并沿着该路径推测性地执行。但是,如果沿着这条路径弹出另一个缓慢的条件分支会发生什么(当然,假设第一个条件尚未解决并且 CPU 不能只提交更改)?CPU只是在推测里面推测吗?如果最后一个条件被错误预测但第一个条件不是,会发生什么?它只是一路回滚吗?

我在谈论这样的事情:

if (value_in_memory == y){
   // computations
   if (another_val_memory == x){
      //computations
   }
}
4

2 回答 2

4

推测执行是执行的常规状态,而不是乱序 CPU 在看到分支时进入,然后在分支不再运行时离开的特殊模式。

如果您认为不仅分支可能出错,而且许多指令(包括访问内存的指令)对其输入值等都有限制,那么这更容易看出。因此,任何实质性的无序执行都意味着不断的推测,而 CPU 是围绕这个想法构建。

所以“嵌套分支”在这个意义上并没有特别。

现在,现代 CPU 有多种方法可以快速恢复分支预测错误,比从其他类型的故障中恢复更快1。例如,他们可能会在某些分支处对寄存器映射的状态进行快照,以允许在分支位于重新排序缓冲区的头部之前开始恢复。由于在所有分支上进行快照并不总是可行的,因此可能涉及复杂的启发式方法来决定在哪里拍摄快照。

我提到最后一部分是因为它是嵌套分支可能很重要的一种方式:当有很多分支在运行时,您可能会遇到一些与跟踪这些分支以进行恢复相关的微架构限制。更详细的可以看一下“branch order buffer”的专利(Intel的技术,当然还有其他的)。


1基本的恢复方法是一直执行到下一条出错的指令退出,然后丢弃所有较年轻的指令。在分支错误预测的情况下,这意味着您实际上可能会遭受两个或更多错误预测,只有最旧的错误预测实际生效:例如,一个较年轻的分支错误预测,并且在执行到该分支时(此时可能会发生恢复),另一个发生错误预测,因此年轻的最终被丢弃。

于 2019-12-06T22:29:12.507 回答
4

(也许不是一个完整的答案,但是当@BeeOnRope 发布答案时,我已经写了一些。无论如何发布这个以获得更多链接和技术细节,以防有人好奇。)


一切都是投机的,直到它退休并成为非投机的,肯定发生的,架构状态的一部分。

例如,任何负载都可能因地址错误而出现故障,任何负载都可能div因被零除而陷入困境。另请参阅乱序执行与推测执行Skylake CPU 错误预测分支时会发生什么?提到分支错误预测特别处理的,因为它们预计会很频繁。快速恢复可以在错误预测的分支达到退休之前开始,这与例如故障负载的行为不同。(这就是为什么 Meltdown 可以被利用的部分原因。)

因此,即使是“常规”指令在提交之前也是推测性地执行的,它们之间的唯一区别是人为的区别,而不是计算机制造的区别?那么,我假设 CPU 存储了多个可能的回滚点?例如,如果我有可能导致页面错误的加载指令或只是使用过时的值,在条件分支中,CPU 会识别这些指令和场景并为每个指令和场景保存一个状态?我觉得我理解错了,因为这可能会导致大量存储寄存器状态和复杂的依赖关系。

退休状态始终是一致的,因此您始终可以回滚到那里并丢弃所有正在进行的工作,例如,如果外部中断到达,您想要处理它,而无需等待一连串缓存未命中加载全部执行。 当中断发生时,流水线中的指令会发生什么?

这种跟踪基本上是免费发生的,或者是您无论如何都需要做的事情才能检测到哪条指令出错,而不仅仅是某处出现问题。(这称为“精确例外”)

人类可以有用地做出的真正区别是在执行非错误案例期间很有可能出错的推测。如果您的代码得到一个错误的指针,那么它的执行方式并不重要;它会出现页面错误,与本地 OoO 执行详细信息相比,这将非常慢。


您说的是现代无序 (OoO)执行(不仅仅是获取)CPU,例如现代 Intel 或 AMD x86、高端 ARM、MIPS r10000 等。

前端是有序的(沿着预测路径进行推测),从无序后端到非推测性退休状态的提交(也称为退休)也是如此。(又名已知良好的建筑状态)。

CPU 使用两种主要结构来跟踪后端的指令(或在 x86 上,uops = 部分指令)。前端的最后阶段(在获取/解码之后)分配/重命名指令并将它们一次添加到这两个结构中。

  • RS = 预留站 = 调度器:尚未执行的指令,等待执行单元。RS 跟踪依赖关系并将最旧的就绪微指令发送到就绪的执行单元。
  • ROB = ReOrder Buffer:尚未退役的指令。指令按顺序进入和离开,因此它可以只是一个循环缓冲区。

    包括一个标志,用于将每个条目标记为已执行或未执行,一旦 RS 将其发送到报告成功的执行单元就设置。ROB 中所有已完成执行位设置的最旧指令都可以“退出”。

    还包括一个标志,指示“如果达到退休则有故障”。例如,这避免了花费时间处理来自错误执行路径上的加载指令的页面错误(很可能有指向未映射页面的指针)。要么在分支错误预测的阴影下,要么就在另一条本应首先出现故障但 OoO exec 稍后处理的指令之后(按程序顺序)。

(我还将寄存器重命名到一个大型物理寄存器文件中。这就是“重命名”部分。分配包括选择指令将使用的执行端口,以及为内存指令保留加载或存储缓冲区条目。)

(还有一个存储缓冲区;存储不直接写入 L1d 缓存,它们写入存储缓冲区。这使得可以推测性执行存储并仍然回滚,而不会使它们对其他内核可见。它还解耦了缓存-未执行存储。一旦存储指令退出,存储缓冲区条目“毕业”并有资格提交到 L1d 高速缓存,一旦 MESI 获得对高速缓存行的独占访问权,并且一旦满足内存排序规则。)


执行单元检测一条指令是否应该出错,或者是否被错误推测并应该回滚,但在指令达到退休之前不一定要采取行动。

有序退出是在 OoO exec 之后恢复程序顺序的步骤,包括错误推测异常的情况。


术语:当指令从前端发送到 ROB + RS 时,英特尔将其称为“问题”。其他计算机体系结构的人通常称之为“调度”。

将微指令从 RS 发送到执行单元被英特尔称为“调度”,其他人称为“发布”。

于 2019-12-10T06:22:05.867 回答