【问题标题】:x86 Program Counter abstracted from microarchitecture?从微体系结构中抽象出来的 x86 程序计数器?
【发布时间】:2019-12-01 22:00:30
【问题描述】:

我正在阅读RISC-V Reader: An Open Architecture Atlas一书。为了解释 ISA(指令集架构)与特定实现(即微架构)的隔离,作者写道:

架构师的诱惑是在 ISA 中包含有助于在特定时间提高一个实现的性能或成本的指令,但会给不同的或未来的实现带来负担。

据我了解,它指出,在设计 ISA 时,ISA 最好避免公开实现它的特定微架构的细节。


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

x86 程序计数器是从微架构中抽象出来的吗?

【问题讨论】:

  • 正在寻找答案,但是不,x86 指令解码已经需要知道一条指令的开始和结束地址才能解码+执行它。它不像 ARM,其中 PC = 2 条指令; that 暴露了流水线获取/解码。 call 推送返回地址并没有真正暴露任何东西。在 x86-64 RIP 相对寻址之前,这基本上是读取 EIP 的唯一方法。

标签: x86 cpu-architecture riscv instruction-set program-counter


【解决方案1】:

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

在 MIPS 和 x86 中,PC 相对寻址模式仅在早期 ISA 版本的分支中发现。后来的版本增加了 PC 相对地址计算(例如 MIPS auipc 或 x86-64 的 LEA 或加载/存储的 RIP 相对寻址模式)。这些都是相互一致的:偏移量是相对于(过去的)指令结尾(即下一条指令开始)编码的——而正如你所注意到的,在 RISC V 中,编码的分支偏移量(和 auipc , etc..) 是相对于指令的开头。

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

(当然,RISC V 仍然需要为 pc-next 和调用指令的返回地址生成指令 + 4,但这在关键路径上要少得多。请注意,在下图中,都没有显示捕获pc+4 作为返回地址。)


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

MIPS 数据路径(简化)


RISC V 数据路径(简化)

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


图表的归属


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

当然,我不能肯定。在我看来,有一个选择要做,而且对于最早的实现来说根本不重要,所以他们可能甚至没有意识到潜在的问题。几乎每条指令都需要计算下一条指令,所以这似乎是合乎逻辑的选择。

充其量,他们可能节省了几条线,因为其他指令(例如调用)确实需要 pc-next 并且不一定需要 pc+0。

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

8086 没有流水线化(指令预取缓冲区除外),可变长度解码在开始执行之前已经找到指令的结尾。

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

我怀疑他们是否做出了与分支延迟槽 (MIPS) 一样的明智决定。


根据 cmets 中的讨论,8086 可能没有任何推送指令起始地址的异常。与后来的 x86 模型不同,除法异常将指令地址推到 div/idiv 之后。而在 8086 中,cs rep movsb(或其他字符串指令)之后的中断恢复推送了最后一个前缀的地址,而不是包括多个前缀的整个指令。这个“错误”记录在Intel's 8086 manual (scanned PDF) 中。所以很有可能8086真的没有记录指令的起始地址或长度,只记录了开始执行前解码完成的地址。这个was fixed by at least 286,可能是 186,但适用于所有 8086 / 8088 CPU。

MIPS 从一开始就有虚拟内存,因此它确实需要能够记录错误指令的地址,以便在异常返回后重新运行。此外,软件 TLB 未命中处理还需要重新运行错误指令。但是异常很慢并且无论如何都会刷新管道,并且直到获取之后才检测到,因此无论如何都需要进行一些计算。

【讨论】:

  • 甚至第一代 x86 (8086) 流水线指令预取与其他非流水线解码/执行 CPU 内部部件分开。但这可能是前面的多个指令;并且不知道指令边界,因此当call 需要读取它时,它不一定仍然持有下一条指令获取地址。但是解码确实必须计算出一条指令作为解码的一部分有多长。 (或者更有可能,只记录它的开始和结束地址)。如果 8086 有任何异常会推送错误指令的地址(如 386 #PF),则可能需要两者。
  • 我不明白为什么那个加法器会以任何方式影响性能。在获取指令之前并不需要下一条指令的地址。所以加法器与取指令并行工作。有这方面的研究吗?这个答案看起来不对。
  • 那么您的答案应该至少讨论这些替代实现中的一种,以支持您的主张。我想不出一种情况,在 RISC-V 中定义 PC 的方式比在 x86 中定义的方式有任何优势(在性能、能量或面积方面)。这实际上只是 ISA 的一个架构特性,我猜它可能会影响 ISA 的设计(但不会以任何重要的方式实现)。
  • @Peter Cordes:8086/8088 上的除法异常没有指向错误指令。 css.csail.mit.edu/6.858/2014/readings/i386/s14_07.htm "在 8086/8088 上,CS:IP 值指向下一条指令。"
  • @Peter Cordes:我认为当重复的字符串操作被中断时,会使用指令的开头(或者更确切地说,第一个前缀的开头)。 (这有一个著名的错误,即原始代删除除最后一个前缀之外的所有内容。也就是说,如果“rep cs movsw”被中断,处理器将重新启动,“cs movsw”丢失了代表前缀。但这被认为是一个错误并在后几代处理器中修复。)
【解决方案2】:

据我了解,它指出在设计 ISA 时,ISA 理想情况下,应避免暴露特定的细节 实现它的微架构。

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

x86 程序计数器是从微架构中抽象出来的吗?

在@InstructionPointer 对其进行编辑之前,您在此问题中提到了 x86 的“第一个实现”,即 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 没有暴露任何这些微架构细节。在现代 Intel 处理器中,可能有许多正在运行的指令,因此每条指令都需要携带足够的信息来重建其地址或下一条指令的地址。

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


脚注:

(1) 解码器可能仍然必须能够识别预取指令是有效的并发出相应的微指令。然而,这种负担不是定义微架构相关指令的结果,而是定义新指令的结果,与这些指令的功能无关。

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

【讨论】:

  • 8086 单独解码前缀(一次 1 个周期)并且对总指令长度有 no 限制。例如一个充满 rep 前缀的 64kiB CS 段将永远循环 IIRC,无论那里是否有操作码或 just 前缀。但是,是的,我认为上限是 6 个字节,不包括任何前缀。操作码 + modrm + disp16 + imm16。有趣的事实:8088 只有一个 4 字节的预取缓冲区,低于 8086 中的 6 个,但显然在总线接口之外没有电路差异。所以预取缓冲区也不是解码缓冲区,实际上只是预取。
  • @PeterCordes 啊哈,控制转移指令(调用和 jmp)的大小如何?它们的长度有限制吗?取指单元实际上只需要保持控制传输指令的长度。就获取单元而言,任何其他指令的长度都可以视为零。
  • felixcloutier.com/x86/call call far ptr16:16 是 5 个字节:操作码 + new_IP + new_CS 是 5 个字节。即使分支目标本身是绝对的,而不是相对的,它也必须推送 CS:IP 返回地址。使用重复的段覆盖前缀,call [mem] 可以是任意长度。或者我猜想call rel16 上的无用前缀也可以是任何长度。这可能是 x86 从头计算的一个很好的理由,而不是从头开始!
  • 对于像 RISC-V 这样的固定指令宽度的 ISA,您的答案中的所有推理当然 非常 不同,您可以在给定的情况下计算指令的开始结束地址,或使用并行运行的加法器尽可能提前计算(假设没有分支)。 8086 显然在设计时并未考虑超标量实现(后来添加到可变长度编码的复杂性导致了当前的灾难)。对于 8086,甚至可能连流水线式 CISC 实现都没有出现。直到 486 和 586 才发生这种情况。
  • 确实如此。获取并保存指令起始地址的 16 位快照(在解码开始之前)可能比累积长度更明智。嗯,我想知道 8086 在处理冗余的lockrep 和段前缀时如何处理异步中断。我想知道该机制是否与某些 8086 CPU 中的 cs/es/ss rep movs 错误(@ecm 提出)有关,其中中断返回地址仅指向最后一个前缀,从而改变了恢复指令的含义。只有字符串指令通常是可中断的,AFAIK;也许前缀解码不是。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-09-11
  • 1970-01-01
  • 2015-02-20
  • 2015-05-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多