延迟槽的作用是什么?

25

据我理解,延迟槽指的是当分支指令被调用时,紧接着分支后面的指令也会从内存中加载。这有什么意义呢?难道我们不期望如果分支被执行,则其后的代码将不会运行吗?这是为了在分支未被执行时节省时间吗?

我正在查看一个流水线图表,似乎分支后面的指令仍然会被执行。


9
你理解CPU流水线的概念吗? - Oliver Charlesworth
1
好的,那么这就是需要关注的重点;一旦你清楚了同时有多个指令在执行,分支延迟槽为什么存在就会变得显而易见。 - Oliver Charlesworth
5
另一个需要考虑的问题是,RISC 架构最初的目标之一是达到每个时钟周期执行一条指令的极限。由于跳转指令需要两个时钟周期,因此需要执行跳转后放置的指令。其他架构可能会执行这个指令,并使用复杂的方案来不提交其结果,以模拟他们没有处理该指令。 - fjardon
5个回答

30

现在大多数处理器都使用流水线技术。《计算机体系结构:量化研究方法》这本书中提到的思想和问题无处不在。在最初的编写时,我认为实际硬件与流水线技术非常吻合。它的四个主要阶段是取指令(Fetch)、译码(Decode)、执行(Execute)和写回(Write back)。

基本上,流水线就像一条装配线,有四个主要阶段,并且最多可以同时处理四条指令。这也让人困惑于执行一条指令需要多少个时钟周期的概念。虽然执行一条指令需要超过一个时钟周期,但如果有一些或者许多指令在并行执行,那么平均执行速度可以接近或超过每个时钟周期一条指令。

当进行分支操作时,流水线会失败。取指令和译码阶段的指令必须被丢弃,你需要重新开始填充指令,因此你需要耗费一些时钟周期用于取指令、译码,然后才能继续执行。分支延迟插槽的思想就是为了挽回其中一个时钟周期的损失。如果声明分支后的指令总是会执行,则在分支被执行时,译码插槽中的指令也会被执行,取指令插槽中的指令将被丢弃,因此你只需要填充一个时间空白而不是两个。所以,现在流水线执行阶段的顺序是执行、执行、空置、执行、执行……分支的开销减少了50%,整体平均执行速度得到改善等等。

ARM处理器没有延迟插槽,但它同样也通过声明程序计数器向前两条指令的假象来实现流水线技术。任何依赖于程序计数器(pc-relative addressing)的操作都必须使用向前两条指令的pc来计算偏移量。对于ARM指令而言,原始Thumb指令的偏移量为4字节,并且加入Thumb2指令后就变复杂了。

目前在学术领域之外,这些都是假象,管道更深,有很多诀窍等等,以便保持传统代码的工作状态,或者不必为每个架构更改重新定义指令工作方式(想象一下MIPS rev x,1个延迟插槽,rev y 2个延迟插槽,rev z如果条件a为3个插槽并且如果条件b为2个插槽,则处理器继续执行分支后的第一个指令,然后丢弃其他几个指令,因为它重新填充管道。实际上,管道的深度往往不会与公众共享。

我看到有关这是RISC问题的评论,可能起源于此,但CISC处理器使用完全相同的技巧,只是给出传统指令集的错觉,有时CISC处理器不过是具有包装器以模拟遗留CISC指令集(微码化)的RISC或VLIW内核。

观看"How It's Made"节目。想象一条装配线,每个步骤都有一个任务。如果生产线上的某个步骤用完了蓝色小零件,并且为了制造蓝色和黄色产品,您需要蓝色小零件。但由于某个人搞砸了,您在下一个星期内无法获得新的蓝色小零件。因此,您必须停止生产线,更改每个阶段的供应,并暂时制造红色和绿色产品,这通常可以在不倒空生产线的情况下适当引入。这就像在分支中发生的事情,装配线深处的某个地方,某些原因导致生产线必须更改,倒掉生产线。延迟插槽是从生产线上挽回一个产品的方法,使得N+1个产品每次生产运行时出来,而不是N个产品。代码执行就像突发式的生产运行,您经常在进入另一个短执行路径的分支之前获得短线性执行路径,然后再进入另一个短执行路径,再分支...


14
请停止破坏您的内容;这是毫无意义的,因为它最终会被还原回旧状态。它所能达到的唯一效果就是让您离开该网站时变得不够庄重。 - Pekka

15
你不会期望在分支被执行时,分支后面的代码不会运行吗?但是现在已经为时过晚了。CPU流水线的整个目的就是希望在每个周期内完成一条指令。唯一能做到这一点的方法是在每个周期内获取一条指令。因此,在CPU注意到必须要执行分支之前,分支指令后面的代码已经被获取并处于执行状态中了。
这有何意义呢?没有任何意义。这不是一项功能,而只是这种流水线设计的产物。

2
如果分支后的指令将一个值增加1,而分支被执行后的指令将其减少1,那么它将无论如何都会先执行增加操作,然后再执行减少操作吗? - James
5
@James: 当然。它们是RISC风格流水线的产物,通常会带来难题。但考虑到它们在这些架构上不可避免地存在,编译器和狡猾的汇编程序员可以利用它们。 - Oliver Charlesworth
2
@James:我怀疑这取决于具体的架构,以及它如何实现断点。 - Oliver Charlesworth
2
还有一个问题。我们能否只是放置一些虚拟指令来解决延迟槽的问题? - James
8
@James:当然,我们可以在那里放置一个“nop”指令(实际上有些编译器会这样做)。但是这是次优的,因为它会浪费一个CPU周期。因此,编译器通常寻找方法来在延迟槽中放置一些有用的指令。 - Oliver Charlesworth
显示剩余3条评论

7
尽管指令出现在分支程序之后,但实际上它是在分支被执行之前运行的。请查看维基百科关于延迟槽分支危险的页面。

6
RISC架构的想法是简化解码并优化流水线以提高速度。 CPU 尝试通过流水线来重叠指令执行,因此多个指令正在同时执行。
延迟槽的重点是执行已经通过部分流水线并且现在位于否则只能被丢弃的槽中的指令。
优化器可以将分支目标的第一条指令移动到延迟槽中,从而免费执行它。
这个特性没有成为主流,主要是因为世界上采用了现有的ISA设计,即x86和x86-64,还有一个原因。
晶体管数量的二次爆炸使得非常复杂的解码器成为可能。当可见的ISA被翻译成微操作时,像延迟槽这样的小技巧变得不重要。
1. ISA: 指令集架构

1
更重要的是,具有更长流水线的较新微架构需要多个“分支延迟”插槽来隐藏由分支引入的获取/解码气泡。像分支延迟插槽这样暴露微体系结构细节对第一代CPU非常有效,但对于相同指令集的更新实现而言,它只是额外的负担,他们必须在实际使用分支预测来隐藏气泡同时支持它。分支延迟插槽中的指令出现页面错误或其他问题很棘手,因为执行必须重新运行它,但仍要进行分支。 - Peter Cordes

5
在流水线实现的教科书示例中,CPU 进行取指译码执行写回操作。这些阶段都在不同的时钟周期中发生,因此实际上每条指令需要 4 个周期才能完成。然而,在第一个操作码即将被译码时,下一个操作码就已经从内存中加载了。当 CPU 被充分占用时,同时处理 4 条不同指令的部分,CPU 的吞吐量为每个时钟周期一条指令。
当机器码中存在以下序列时:
      sub r0, #1
      bne loop
      xxx

处理器可以将sub r0, #1写回阶段的信息反馈到bne loop执行阶段,但同时xxxx已经处于取指阶段。为了简化取消流水线的必要性,CPU设计者选择使用延迟槽。在延迟槽中的指令被获取后,取指单元会有正确的分支目标地址。优化编译器很少需要在延迟槽中放置NOP,而是插入一个在两个可能的分支目标中都必须需要的指令。

+1 表示 优化编译器很少需要在延迟槽中放置 NOP,但我不理解其他部分。 - Kindred

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接