从微体系结构中抽象出来的 x86 程序计数器?

x86 Program Counter abstracted from microarchitecture?

我正在阅读 RISC-V Reader:开放架构图集 这本书。为了解释 ISA(指令集架构)与特定实现(即微架构)的隔离,作者写道:

The temptation for an architect is to include instructions in an ISA that helps performance or cost of one implementation at a particular time, but burden different or future implementations.

据我了解,它指出在设计 ISA 时,ISA 最好不要公开实现它的特定微体系结构的细节。


记住上面的引述:当谈到程序计数器时,在 RISC-V ISA 上,程序计数器 (pc) 指向 当前正在执行的指令。另一方面,在 x86 ISA 上,程序计数器(eip)不包含当前正在执行的指令的地址,而是包含当前指令之后的指令的地址[=28] =].

x86 程序计数器是否从微体系结构中抽象出来?

我将根据 MIPS 而不是 x86 来回答这个问题,因为 (1) MIPS 和 x86 在这方面有相似之处,并且因为 (2) RISC V 是由 Patterson 等人开发的,之后数十年的 MIPS 经验。我觉得他们书中的这些陈述在这个比较中得到了最好的理解,因为 x86 和 MIPS 都编码了相对于指令结尾的分支偏移量(MIPS 中的 pc+4)。

在 MIPS 和 x86 中,PC-relative 寻址模式只存在于早期 ISA 版本的分支中。后来的修订添加了 PC-relative 地址计算(例如 MIPS auipc 或 x86-64 的 RIP-relative LEA 寻址模式或 load/store)。这些都是彼此一致的:偏移量是相对于(过去的)指令结尾(即下一条指令开始)编码的——而正如您所注意到的,在 RISC V 中,编码的分支偏移量(和 auipc等..) 是相对于指令的开始。

它的价值在于它从某些数据路径中删除了一个加法器,有时这些数据路径之一可能在关键路径上,因此对于某些实现来说,数据路径的这种微小缩短意味着更高的时钟速率。

(当然,RISC V 仍然需要为 pc-next 和调用指令的 return 地址生成指令 + 4,但这在关键路径上要少得多。注意在下图均未将 pc+4 捕获为 return 地址。)


让我们比较一下硬件框图:

                                               MIPS datapath (simplified)


                                               RISC V datapath (simplified)

您可以在 RISC V 数据路径图上看到标记为 #5 的线(红色,就在控制椭圆上方)绕过加法器(#4,它为 pc-next 添加 4 到 pc) .


图表归因


为什么 x86 / MIPS 在其初始版本中做出不同的选择?

当然,我不能肯定地说。在我看来,需要做出选择,而这对于最早的实施来说根本无关紧要,因此他们可能甚至没有意识到潜在的问题。无论如何,几乎每条指令都需要计算 instruction-next,因此这似乎是合乎逻辑的选择。

充其量,他们可能节省了一些电线,因为 pc-next 确实是其他指令(例如 call)所必需的,而 pc+0 则不一定需要。

对先前处理器的检查可能表明这正是当时的处理方式,因此这可能更多地是对现有方法的延续,而不是设计选择。

8086 不是流水线(指令预取缓冲区除外)并且 variable-length 解码在开始执行之前已经找到指令的结尾。

事后看来,这个数据路径问题现在已在 RISC V 中得到解决。

我怀疑他们对此做出了同样程度的有意识的决定,例如,对分支延迟槽 (MIPS) 所做的决定。


根据评论中的讨论,8086 可能没有任何推送指令起始地址的异常。与后来的 x86 模型不同,除法异常将指令地址推到 div/idiv 之后。而在 8086 中,cs rep movsb(或其他字符串指令)之后的 interrupt-resume 压入了最后一个前缀的地址,而不是包含多个前缀的整个指令。这个 "bug" 记录在 Intel's 8086 manual (scanned PDF). So it's quite possible 8086 really didn't record the instruction start address or length, only the address where decoding finished before starting execution. This was fixed by at least 286 中,可能是 186,但适用于所有 8086 / 8088 CPU。

MIPS 从一开始就有虚拟内存,所以它确实需要能够记录错误指令的地址,以便它可以在 exception-return 之后重新运行。加上软件 TLB-miss 处理还需要 re-rerunning 错误指令。但是异常很慢并且无论如何都会刷新管道,并且直到获取之后很长时间才被检测到,所以无论如何都可能需要一些计算。

As far as I understand, it states that when designing an ISA, the ISA should ideally refrain from exposing the details of a particular microarchitecture that implements it.

如果您衡量理想 ISA 的指标是简单性,那么我可能会同意您的看法。但在某些情况下,通过 ISA 公开微体系结构的某些特征以提高性能可能是有益的,并且有一些方法可以忽略这样做的负担。例如,考虑 x86 中的软件预取指令。这些指令的行为在架构上被定义为依赖于微架构。英特尔甚至可以在未来设计一种微架构,其中这些指令表现为空操作,而不会违反 x86 规范。唯一的负担是定义这些指令的功能1。但是,如果预取指令在体系结构上被定义为将 64 字节对齐的数据预取到 L3 缓存中,并且没有 CPUID 位来允许对该指令的可选支持,那么这可能确实会使支持这样的指令在未来成为一个沉重的负担.

Is the x86 Program Counter abstracted away from the microarchitecture?

在被@InstructionPointer 编辑之前,您在这个问题中提到了 x86 的 "first implementation",即 8086。这是一个具有两个管道阶段的简单处理器:获取和执行。架构寄存器之一是 IP,它被定义为包含下一条指令的 16 位偏移量(从代码段基址开始)。因此,每条指令的 IP 的体系结构值等于偏移量加上指令的大小。这在 8086 中是如何实现的?实际上没有物理寄存器存储 IP 值。有一个物理指令指针寄存器,但它指向接下来要提取到指令队列中的 16 位,指令队列最多可容纳 6 个字节(参见:https://patents.google.com/patent/US4449184A/en)。如果正在执行的当前指令是控制传输指令,则目标地址是根据指令的相对偏移量、物理IP中的当前值以及有效地址的数量即时计算的指令队列中的字节。例如相对偏移量为15,物理IP为100,指令队列包含4个有效字节,则目标偏移量为:100 - 4 + 15 = 111。然后可以计算出物理地址通过添加 20 位代码段地址。显然,架构 IP 不会暴露任何这些微架构细节。在现代英特尔处理器中,可能有许多指令在运行,因此每条指令都需要携带足够的信息来重建其地址或后续指令的地址。

如果 x86 架构 IP 被定义为指向当前指令而不是下一条指令怎么办?这将如何影响 8086 的设计?好吧,控制传输指令的相对偏移量是相对于当前指令的偏移量,而不是下一条指令的偏移量。在前面的例子中,我们必须从 111 中减去当前指令的长度才能得到目标偏移量。因此可能需要额外的硬件来跟踪当前指令的大小并将其包含在计算中。但是在这样的 ISA 中,我们可以将所有控制传输指令定义为具有统一的长度2(其他指令仍然可以是可变长度),这消除了大部分开销。我想不出一个现实的例子,其中以一种方式定义程序计数器明显优于另一种方式。但是,它可能会影响 ISA 的设计。


脚注:

(1) 解码器可能仍然需要能够识别预取指令是有效的并发出相应的微指令。然而,这种负担不是定义微体系结构相关指令的结果,而是定义新指令的结果,而不管这些指令的功能如何。

(2) 或者,当前指令的长度可以存储在一个微型寄存器中。 IIRC,8086 中的最大指令长度为 6 个字节,因此最多需要 3 位来存储任何指令的长度。即使是8086天,这个开销也很小。