为什么分支延迟槽被弃用或过时?

Why is the branch delay slot deprecated or obsolete?

当我阅读 RISC-V 用户级 ISA 手册时,我注意到它说 "OpenRISC has condition codes and branch delay slots, which complicate higher performance implementations." 所以 RISC-V 没有分支延迟槽 RISC-V User-Level ISA manual link. Moreover,Wikipedia 说大多数较新的 RISC设计省略了分支延迟槽。为什么大多数较新的 RISC 体系结构逐渐省略分支延迟槽?

延迟槽仅对短的有序标量流水线有用,对高性能超标量流水线没有帮助,尤其是乱序执行的流水线。

它们使异常处理显着复杂化(对于硬件和软件),因为您需要记录当前程序计数器和单独的下一个 PC 地址,以防延迟槽中的指令发生异常。

它们还通过引入多种可能性使 复杂化,例如分支延迟指令已经在管道中并且需要 not 被杀死,而不是仍在等待I-cache 未命中,因此重新引导前端需要等到它获取分支延迟指令之后。


分支延迟槽在架构上公开了有序经典 RISC 管道的实现细节,以提高这种 uarch 的性能,但其他任何事情都必须解决它。如果您的 uarch 是标量 classic RISC.

,它只会避免从采用的分支中获取代码气泡(即使没有分支预测)

即使是现代有序的 uarch 需要 分支预测以获得良好的性能,内存延迟(以 CPU 时钟周期测量)比现在高得多早期的 MIPS。

(有趣的事实:MIPS 的 1 个延迟槽足以隐藏 R2000 MIPS I 上的总分支延迟,这要归功于 将其降低到 1 个周期。)


分支延迟槽不能总是被编译器最佳地填充,所以即使我们可以在没有显着开销的情况下以高性能 CPU 实现它们,它们确实会在每个完成的总工作量方面消耗吞吐量操作说明。程序通常需要执行更多指令,而不是更少,在 ISA 中有延迟槽。

(虽然有时在 之后做一些无条件的事情,比较和分支可以允许重用寄存器而不是需要一个新的寄存器,在没有标志的 ISA 上,比如 MIPS where branch指令直接测试整数寄存器。)

引用 Henessy 和 Patterson(计算机体系结构与设计,第 5 版)

Fallacy : You can design a flawless architecture.
All architecture design involves trade-offs made in the context of a set of hardware and software technologies. Over time those technologies are likely to change, and decisions that may have been correct at the time they were made look like mistakes. (...) An example in the RISC camp is delayed branch. It was a simple matter to control pipeline hazards with five-stage pipelines, but a challenge for processors with longer pipelines that issue multiple instructions per clock cycle.

确实,在软件方面,延迟分支只有缺点,因为它使程序更难阅读并且效率更低,因为插槽经常被 nops 填充。

在硬件方面,这是八十年代的一项技术决策,当时流水线是 5 或 6 级,无法避免单周期分支惩罚。

但目前,管道要复杂得多。在最近的奔腾微架构上,分支惩罚是 15-25 个周期。因此,一个指令延迟分支是无用的,并且试图用 15 条指令延迟分支隐藏这个延迟槽(这会破坏指令集兼容性)将是无稽之谈,显然是不可能的。

而且我们开发了新技术。分支预测是一项非常成熟的技术。使用当前的分支预测器,错误预测远低于具有无用 (nop) 延迟槽的分支数量,因此效率更高,即使在 6 周期计算机(如 nios-f)上也是如此。

因此延迟分支在硬件和软件方面的效率较低。没有理由保留它们。

分支延迟槽是在最早的单期、有序 RISC 实现中作为性能解决方法引入的。早在这些架构的第二个商业实现中,延迟槽和单一条件代码的概念就已经很清楚了。当我们在 HaL 开发 64 位 SPARC 架构时,寄存器 windows 已添加到该列表中。综合挑战足以让我们提议使用动态二进制翻译来支持 SPARC32,这样我们就可以放弃遗留负担。他们当时的成本是芯片面积的40%和指令发布率的20%到25%。

现代处理器实现严重乱序(阅读 "register renaming" 或 "Tomasulo's algorithm")、动态调度,并且在许多情况下是多问题的。因此,延迟分支已经从一个性能增强变成了一个复杂的问题,为了兼容性,指令排序单元和寄存器重命名逻辑必须小心绕过。

坦率地说,这在 SOAR/SPARC 或 MIPS 芯片上都不是一个好主意。延迟分支为调试器中的单步执行、动态二进制翻译器和二进制代码分析(我已经一次或多次实现了所有这些)带来了有趣的挑战。即使在单一问题的机器上,他们也为异常处理创造了一些有趣的复杂性。早在这些指令集的第二次商业实施中,延迟槽和单一条件代码概念就已经成为障碍。

A​​lain 关于 Pentium 分支成本的评论并没有直接转移到 RISC 部分,而且这个问题比他建议的要复杂一些。在固定长度的指令集上,实现一个叫做 "branch target buffer" 的东西很简单,它将指令缓存在分支目标上,这样分支就不会出现流水线停顿。在最初的 RISC 机器(IBM 603)上,John Cocke 合并了一条 "prepare to branch" 指令,其目的是允许程序(或更准确地说,编译器)将可能的目标显式加载到分支目标缓冲区中。在一个好的实现中,BTB 中的指令是预解码的,这会减少流水线中的一个周期,并使通过 BTB 的正确预测转换几乎是免费的。那时的问题是条件代码和错误预测。

由于 BTB 和多问题,需要重新构想分支延迟和分支预测错误延迟的概念。在许多多任务机器上实际发生的是处理器沿着分支的 两条 路径前进——至少当它可以从指令获取单元中当前预加载的缓存行中获取指令时或 BTB 中的说明。这具有减慢分支两侧的指令问题的效果,但也可以让您在分支的两侧进行 progress。当分支解析时,"should not have taken" 路径被放弃。对于整数处理,这会减慢你的速度。对于浮点数,不太清楚,因为计算操作需要几个周期。

在内部,一个积极的多问题机器很可能在分支时有三四个操作在内部排队,所以分支延迟通常可以通过执行这些已经排队的指令然后重新构建队列深度。