乱序指令执行:提交顺序是否保留?

und*_*ind 6 cpu cpu-architecture dynamic-execution instructions pipelining

一方面,维基百科写了乱序执行的步骤:

  1. 取指令。
  2. 指令分派到指令队列(也称为指令缓冲区或保留站)。
  3. 指令在队列中等待,直到其输入操作数可用。然后允许该指令在较早、较旧的指令之前离开队列。
  4. 指令被发布到适当的功能单元并由该单元执行。
  5. 结果在排队。
  6. 只有在所有较旧的指令将其结果写回寄存器文件后,才会将该结果写回寄存器文件。这称为毕业或退休阶段。

类似的信息可以在《计算机组织与设计》一书中找到:

为了让程序表现得像在一个简单的有序流水线上运行,指令获取和解码单元需要按顺序发出指令,这允许跟踪依赖关系,并且提交单元需要将结果写入寄存器和程序获取顺序中的内存。这种保守的模式被称为有序提交……今天,所有动态调度的管道都使用有序提交。

因此,据我所知,即使指令以乱序方式执行,其执行结果也会保存在重新排序缓冲区中,然后以确定性顺序提交到内存/寄存器。

另一方面,有一个众所周知的事实,即现代 CPU 可以为性能加速目的重新排序内存操作(例如,可以重新排序两个相邻的独立加载指令)。维基百科在这里写到。

您能否解释一下这种差异?

Pet*_*des 8

TL:DR:内存排序与乱序执行不同。它甚至发生在有序流水线 CPU 上。

有序提交对于精确的异常是必要的1,可以回滚到发生故障的指令,之后没有任何指令已经退出。乱序执行的主要规则是不要破坏单线程代码。如果您在没有任何其他机制的情况下允许无序提交(退休),则可能会发生页面错误,而一些后来的指令已经执行过一次,和/或一些较早的指令尚未执行。这将使在处理页面错误后无法以正常方式重新启动执行。

(在没有异常的正常情况下,按顺序发布/重命名和依赖项跟踪负责正确执行。)

内存排序与其他内核看到的内容有关。另请注意,您引用的内容仅涉及将结果提交到寄存器文件,而不是内存。

(脚注 1 : Kilo-instruction Processors: Overcoming the Memory Wall是一篇关于检查点状态的理论论文,它允许在异常发生之前的某个时刻回滚到一致的机器状态,允许更大的无序窗口而不需要巨大的 ROB大小。AFAIK,没有主流商业设计使用它,但它表明除了严格按顺序退休之外,理论上还有其他方法可以构建可用的 CPU。

据报道,Apple 的 M1 的乱序窗口比 x86 的同时代产品要大得多,但我没有看到任何明确的信息表明它使用了一个非常大的 ROB 以外的任何东西。)


由于每个内核的私有 L1 缓存与系统中的所有其他数据缓存一致,因此内存排序是指令何时读取或写入缓存的问题。这与他们从乱序核心退休的时间分开。

当负载从缓存中读取数据时,负载变得全局可见。这或多或少是在他们“执行”时,而且绝对是在他们退休(又名提交)之前。

当它们的数据被提交到缓存时,存储变得全局可见。这必须等到知道它们是非推测性的,即没有异常或中断会导致必须“撤消”存储的回滚。因此,当存储从乱序核心退出时,它就可以提交到 L1 缓存。

但即使是有序 CPU 也使用存储队列或存储缓冲区来隐藏 L1 缓存中未命中的存储延迟。一旦知道它肯定会发生,乱序机器就不需要继续跟踪存储,因此存储 insn/uop 甚至可以在它提交到 L1 缓存之前退出。存储缓冲区保留它,直到 L1 缓存准备好接受它。即当它拥有缓存行时(MESI 缓存一致性协议的独占或修改状态),并且内存排序规则允许存储现在变得全局可见。

另请参阅我对写入分配/获取写入缓存策略的回答

据我了解,当商店的数据在乱序核心中“执行”时,它会被添加到商店队列中,这就是商店执行单元所做的。(存储地址写入地址,存储数据将数据写入在分配/重命名时为其保留的存储缓冲区条目,因此这两个部分中的任何一个都可以首先在这些部分单独调度的 CPU 上执行,例如 Intel。 )

加载必须探测存储队列,以便它们看到最近存储的数据。


对于像 x86 这样的具有强排序的 ISA,存储队列必须保留 ISA 的内存排序语义。即商店不能与其他商店重新排序,并且商店不能早期加载之前变得全局可见。(LoadStore 重新排序是不允许的(StoreStore 或 LoadLoad 也不允许),只有 StoreLoad 重新排序)。

David Kanter关于如何以与 Haswell 不同的方式实现 TSX(事务内存)文章提供了对内存顺序缓冲区的一些见解,以及它如何与跟踪指令/uop 重新排序的 ReOrder 缓冲区 (ROB) 分离的结构。他首先描述了当前的工作方式,然后再介绍如何修改它以跟踪可以作为一个组提交或中止的事务。