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

7
一方面,维基百科介绍了乱序执行的步骤:
  1. 指令获取。
  2. 指令分派到指令队列(也称为指令缓冲区或保留站)。
  3. 指令在队列中等待其输入操作数可用。然后允许该指令在早于它的、更老的指令之前离开队列。
  4. 将指令发出到适当的功能单元,并由该单元执行。
  5. 结果排队等待。
  6. 只有在所有更老的 指令将其结果写回寄存器文件之后,才会将此结果写回寄存器文件。这被称为毕业或退休阶段。

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

为了使程序表现得好像它们在简单的按顺序管道上运行一样,指令获取和解码单元需要按顺序发布指令,这允许跟踪依赖关系,而提交单元需要按程序获取顺序将结果写入寄存器和内存。这种保守模式称为按顺序提交... 今天,所有动态调度的管道都使用按顺序提交。

据我了解,即使指令执行是按照乱序方式进行的,它们的执行结果也会被保存在重新排序缓冲区中,然后按照确定的顺序提交到内存/寄存器中。
另一方面,已知现代CPU可以重新排序内存操作以加速性能(例如,两个相邻的独立加载指令可以被重新排序)。维基百科在这里写了相关内容。
您能否解释一下这种差异?

1
“commit”这个词实际上有点模糊。如果你按照字面意思来定义它,那么几乎没有任何OOE的余地。在使用其结果之前,您不需要等待“commit”的指令。我不清楚它内部的工作原理。它几乎肯定与从分支预测和内存消歧中进行的推测恢复密切相关。 - Mysticial
至少,每个指令都将具有多个类似于“提交”的阶段:1)当输出准备好用于另一个指令时。2)当指令不再处于推测状态时。3)当指令从重排序缓冲区中删除时。您的负载示例不仅限于负载,而几乎适用于任何写入寄存器的指令。 - Mysticial
@Mysticial:我相当确定“commit”在这里被用作“退役”的同义词。 这只能发生在指令执行完成且已知为非推测时(即当所有前面的指令都没有故障地退役时)。 - Peter Cordes
顺便说一下,已经有一些关于乱序退休的研究,同时通过使用检查点来回滚以便在检测到异常时保持精确。例如,这篇关于Kilo-Instruction处理器的论文http://csl.cornell.edu/~martinez/doc/taco04.pdf非常有趣。(Kilo-instruction指的是乱序重新排序窗口相当于1k的ROB大小,允许隐藏缓存未命中延迟的这么大的窗口,而实际上并不难以构建)。@Mysticial - Peter Cordes
1个回答

8
TL:DR:内存排序不同于乱序执行。即使在顺序流水线CPU上也会发生。
顺序提交对于精确异常是必要的,可以回滚到恰好出现故障的指令,而没有任何已经退役的指令。乱序执行的基本规则是“不要破坏单线程代码”。如果允许乱序提交(退役)而没有任何其他机制,可能会在一些稍后的指令已经执行一次,或者一些早期的指令尚未执行时发生页面错误。这将使得在正常方式下处理页面错误后重新启动执行变得不可能。
(顺序问题/重命名和依赖跟踪处理了正常情况下的正确执行。)
内存排序关乎其他核心所看到的内容。还要注意,你引用的内容只谈到将结果提交到寄存器文件中,而不是提交到内存中。
(脚注1: 克服内存墙的千指令处理器是一篇关于将状态检查点化以允许在异常发生前回滚到一致的机器状态的理论论文,这样可以使用更大的乱序窗口而不需要那么巨大的ROB。据我所知,没有主流商业设计采用这种方法,但它表明在构建可用CPU时,除了严格按顺序退役之外还有其他方法。)
(据报道,苹果的M1拥有比其x86同行更大的乱序窗口,但我还没有看到任何确切的信息表明它使用的不仅仅是一个非常大的ROB。)
由于每个核心的私有L1缓存与系统中所有其他数据缓存一致,因此内存排序是关于指令何时读取或写入缓存的问题。这与它们从乱序核心退役的时间无关。
当加载指令从缓存中读取其数据时,它们变得全局可见。这更或多或少是它们“执行”的时间,并且绝对早于它们退役(也称提交)的时间。
当存储指令的数据被提交到缓存时,它们变得全局可见。这必须等待它们被确认为非推测性,即没有异常或中断会导致必须“撤消”存储的回滚。因此,存储可以在从乱序核心退役时尽早提交到L1缓存。
但即使是顺序CPU也会使用一个存储队列或存储缓冲区来隐藏未命中L1缓存的存储器延迟。乱序机制在确定存储器一定发生后就不再需要跟踪其状态,因此存储指令/微操作可以在提交到L1缓存之前就退役。存储缓冲区会将其保留,直到L1缓存准备好接受它,即当它拥有缓存行(MESI缓存一致性协议的独占或修改状态)并且内存排序规则允许存储器现在变为全局可见。

请参阅我的写分配/写取回高速缓存策略的回答。

据我所知,当一个存储器在乱序核心中“执行”时,其数据会添加到存储器队列中,这就是存储器执行单元的作用。(存储器地址将地址写入,存储器数据将数据写入存储缓冲区条目,在分配/重命名时为其保留,因此在那些部件被单独调度的CPU上,其中任何一个部分都可以首先执行,例如英特尔。)加载必须探测存储器队列,以便它们看到最近存储的数据。
对于像x86这样具有强排序特性的ISA,存储队列必须保留ISA的内存排序语义。也就是说,存储不能与其他存储重新排序,并且存储不能在早期加载之前变为全局可见(不允许LoadStore重新排序(也不允许StoreStore或LoadLoad),只允许StoreLoad重新排序)。
David Kanter在文章中描述了TSX(事务内存)可以如何以不同于Haswell的方式实现,提供了一些关于内存序缓冲区(Memory Order Buffer)的见解,以及它如何作为一个单独的结构跟踪指令/微操作的重排序。他首先描述了当前的工作原理,然后介绍了如何修改来跟踪可以作为一组提交或中止的事务。

1
“按顺序提交”使当前核心的代码看起来像是按顺序运行。您是指当前线程自己的代码吗?如果是这样,即使指令乱序退役(遵守RAW/WAR/WAW危险),线程也应该按顺序查看自己的代码。在同一核心上运行的两个线程不会观察到重新排序,因为按顺序提交(加上精确中断/上下文切换)。 - Daniel Nitzan
1
@DanielNitzan:确实,那个措辞有误导性。按顺序发出可以处理无异常情况下的正确性。乱序提交的实际问题是您会丢失精确的异常信息:例如,在一些早期指令执行之前或在一些后续指令执行之后看到页面错误发生。(这将使得在页面错误后重新启动执行变得不可能,这就是为什么没有人以这种方式构建普通CPU的原因...) - Peter Cordes
@DanielNitzan:已更新,感谢您在这个旧答案中发现了那个错误。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接