【问题标题】:Why is the branch delay slot deprecated or obsolete?为什么分支延迟槽被弃用或过时?
【发布时间】:2019-07-10 11:41:39
【问题描述】:
【问题讨论】:
标签:
cpu
cpu-architecture
riscv
【解决方案1】:
引用 Henessy 和 Patterson(计算机体系结构和设计,第 5 版)
谬误:您可以设计出完美无瑕的架构。
所有架构设计都涉及在一组硬件和软件技术的背景下进行的权衡。随着时间的推移,这些技术可能会发生变化,而在做出这些决定时可能是正确的决定看起来像是错误的。 (...) RISC 阵营的一个例子
是延迟分支。控制管道是一件简单的事情
五级流水线的危害,但对于更长的处理器来说是一个挑战
每个时钟周期发出多条指令的流水线。
确实,就软件而言,延迟分支只有缺点,因为它使程序更难阅读,效率更低,因为槽经常被 nop 填满。
在硬件方面,这是一个在八十年代有一定意义的技术决策,当时流水线是 5 或 6 个阶段,无法避免单周期分支惩罚。
但目前,管道要复杂得多。在最近的 pentium μarchitectures 上,分支惩罚是 15-25 个周期。因此,一条指令延迟分支是无用的,尝试用 15 条指令延迟分支隐藏这个延迟槽是无稽之谈,显然不可能(这会破坏指令集的兼容性)。
我们开发了新技术。分支预测是一项非常成熟的技术。使用目前的分支预测器,错误预测远低于具有无用 (nop) 延迟槽的分支数量,因此即使在 6 个周期的计算机上(如 nios-f)也更有效。
因此延迟分支在硬件和软件方面的效率较低。没有理由保留它们。
【讨论】:
-
现代 x86 也是超标量,放大了这个问题。约 16 个周期的最佳情况分支恢复损失(对于 uop 缓存中的命中:7-cpu.com/cpu/Skylake.html)表示 Sandybridge 系列的前端带宽约 64 uop。当然,并非所有代码都能使前端饱和,因此 15 条指令延迟块可以隐藏 一些 的分支延迟。但是,如果您像 MIPS 那样在延迟块中禁止分支指令,那将完全无法使用。 cmp/jcc 代表动态指令组合的很大部分,例如在一些常见的工作负载 IIRC 上占 25%。
【解决方案2】:
延迟槽仅对短的有序标量管道有用,而不是高性能超标量,尤其是无序执行的管道。
它们使异常处理显着复杂化(对于硬件和软件),因为您需要记录当前程序计数器和单独的下一个 PC 地址,以防延迟槽中的指令发生异常。
它们还通过引入多种可能性使How many instructions need to be killed on a miss-predict in a 6-stage scalar or superscalar MIPS? 复杂化,例如分支延迟指令已经在流水线中并且需要不被杀死,而不是仍在等待 I-cache 未命中所以重新- 引导前端需要等到它获取分支延迟指令之后。
分支延迟槽在架构上公开了有序经典 RISC 管道的实现细节,以提高这种 uarch 的性能,但其他任何事情都必须解决它。如果您的 uarch 是一个标量 classic RISC,它只会避免从所采用的分支中提取代码气泡(即使没有分支预测)。
即使是现代的有序 uarch需要分支预测以获得良好的性能,内存延迟(以 CPU 时钟周期衡量)远高于早期 MIPS 时代。
(有趣的事实:MIPS 的 1 个延迟槽足以隐藏 R2000 MIPS I 上的总分支延迟,这要归功于 clever design 将其保持在 1 个周期。)
编译器无法始终以最佳方式填充分支延迟槽,因此即使我们可以在高性能 CPU 中实现它们而无需显着开销,但就每条指令完成的总工作量而言,它们也会消耗吞吐量。程序通常需要在 ISA 中使用延迟槽执行更多的指令,而不是更少的指令。
(虽然有时在之后做一些无条件的事情,比较和分支可以允许重用寄存器而不需要新的寄存器,在没有像 MIPS 这样的标志的 ISA 上,分支指令直接测试整数寄存器.)
【解决方案3】:
在最早的单问题、有序 RISC 实现中引入了分支延迟槽作为一种性能解决方法。早在这些架构的第二次商业实现时,就已经很清楚延迟槽和单个条件代码的概念都将成为障碍。当我们在 HaL 开发 64 位 SPARC 架构时,寄存器窗口已添加到该列表中。综合挑战足以让我们提议使用动态二进制转换来支持 SPARC32,这样我们就可以放弃遗留的负担。当时它们的成本是芯片面积的 40% 和指令发布率的 20% 到 25%。
现代处理器实现非常混乱(阅读“寄存器重命名”或“Tomasulo 算法”)、动态调度,并且在许多情况下是多问题。因此,延迟分支已经从一种性能增强变成了一种复杂性,为了兼容性,指令排序单元和寄存器重命名逻辑必须小心处理。
坦率地说,在 SOAR/SPARC 或 MIPS 芯片上这也不是一个好主意。延迟分支为单步调试器、动态二进制转换器和二进制代码分析(我曾一次或多次实现所有这些)带来了有趣的挑战。即使在单问题机器上,它们也为异常处理带来了一些有趣的复杂性。早在这些指令集的第二次商业实现时,延迟槽和单一条件代码的概念就已经成为阻碍。
Alain 关于 Pentium 分支成本的评论并没有直接转移到 RISC 部件上,而且这个问题比他建议的要复杂一些。在固定长度的指令集上,很容易实现称为“分支目标缓冲区”的东西,它将指令缓存在分支目标处,这样就不会因分支而导致流水线停顿。在最初的 RISC 机器(IBM 603)上,John Cocke 合并了一条“准备分支”指令,其目的是允许程序(或更准确地说,编译器)将可能的目标显式加载到分支目标缓冲区中。在一个好的实现中,BTB 中的指令是预解码的,这减少了流水线的一个周期,并使正确预测的通过 BTB 的转换几乎是免费的。那时的问题是条件代码和错误预测。
由于 BTB 和多问题,需要重新构想分支延迟和分支错误预测延迟的概念。在许多多问题机器上实际发生的是处理器沿着分支的两个路径前进 - 至少在它可以从指令获取单元中当前预加载的缓存行中获取指令或指令时在 BTB 中。这会减缓分支两侧的指令问题,但也可以让您在分支的两侧都取得 progress。当分支解决时,“不应该采取”的路径被放弃。对于整数处理,这会减慢您的速度。对于浮点数不太清楚,因为计算操作需要几个周期。
在内部,一个激进的多问题机器可能在分支时有三四个操作在内部排队,所以分支延迟通常可以通过执行这些已经排队的指令然后重新构建队列深度。