嵌套分支和推测执行会发生什么?

What happens with nested branches and speculative execution?

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

我说的是这样的:

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

推测执行是常规执行状态,不是乱序CPU看到分支就进入,看到分支就离开的特殊模式已停飞。

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

所以 "nested branches" 在这个意义上并没有变得特别。

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

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


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

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


一切在退休之前总是推测性的,并且变得非推测性,肯定发生了,建筑状态的一部分。

例如any load 可能会因地址错误而出错,any div 可能会陷入被零除的陷阱。另见 That and 提到 branch 错误预测 特殊处理的,因为它们预计会很频繁。快速恢复可以在错误预测的分支退役之前开始,这与例如故障负载的行为不同。 (这就是为什么 Meltdown 可以被利用的部分原因。)

So even "regular" instructions are executed speculatively before being commited, and the only distinction between them is a human-made distinction, not computer-made? I presume, then, that the CPU stores multiple, possible rollback points? For instance if I have load instructions that may lead to page faults or simply use stale values, inside a conditional branch, the CPU identifies such instructions and scenarios and saves a state for each of them? I feel like I misunderstood because this may lead to a lot of storing register states and complicated dependencies.

退役状态始终是一致的,因此您始终可以回滚到那里并放弃所有正在进行的工作,例如如果外部中断到达,您希望处理它而不用等待一连串的缓存未命中加载全部执行。 When an interrupt occurs, what happens to instructions in the pipeline?

此跟踪基本上是免费进行的,或者无论如何您都需要做一些事情才能检测到 哪个 指令出错,而不仅仅是某处出现问题。 (这叫"precise exceptions")

人类可以有效做出的真正区分是推测 在执行非错误情况时确实有可能出错。如果您的代码得到一个错误的指针,那么它的执行方式并不重要;它将发生页面错误,与本地 OoO 执行细节相比,这将非常慢。


你说的是现代乱序 (OoO) 执行(不仅仅是获取)CPU,比如现代英特尔或 AMD x86,高-结束 ARM、MIPS r10000 等

前端是有序的(根据预测路径推测),从无序后端到非推测退休状态的提交(也称为退休)也是如此。 (又名已知良好的架构状态)。

CPU 在后端使用两个主要结构来跟踪指令(或在 x86 上,uops = 部分指令)。前端的最后阶段(获取/解码后)allocates/renames 指令,并立即将它们添加到 both 这些结构中。

  • RS = Reservation Station = scheduler: not-yet-executed 指令,等待一个执行单元。 RS 跟踪依赖关系并将最早就绪的 uops 发送到就绪的执行单元。
  • ROB = 重新排序缓冲区:尚未-退休 指令。指令按顺序进入和离开,因此它可以只是一个循环缓冲区。

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

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

(我还忽略了对大型物理寄存器文件的寄存器重命名。 那是 "rename" 部分。分配包括选择指令将使用哪个执行端口,以及为内存指令保留加载或存储缓冲区条目。)

(还有一个存储缓冲区;存储不直接写入 L1d 缓存,它们写入存储缓冲区。这使得可以推测性地执行存储并仍然回滚,而不会对其他内核可见。它还将缓存未命中存储与执行分离。一旦存储指令退出,存储缓冲区条目 "graduates" 并有资格提交到 L1d 缓存,一旦 MESI 获得对缓存行的独占访问权限,并且一旦内存排序满足规则。)


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

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


术语:当指令从前端发送到ROB + RS时,Intel称之为"issue"。其他计算机体系结构的人通常称之为 "dispatch".

从RS发送微指令到执行单元,Intel叫"dispatch",别人叫"issue"