提问者:小点点

乱序执行vs.推测执行


我读过维基百科关于无序执行和推测执行的页面。

然而,我无法理解的是相似之处和不同之处。在我看来,投机执行在没有确定条件的值时使用无序执行。

当我阅读Meltdown和Spectre的论文并做额外的研究时,困惑出现了。Meltdown论文中说Meltdown是基于乱序执行的,而其他一些资源,包括关于分离执行的wiki页面,则表示Meltdown是基于推测执行的。

我想得到一些澄清。


共2个答案

匿名用户

推测执行和乱序执行是正交的。人们可以设计一个OoO但不推测或推测但有序的处理器。OoO执行是一种执行模型,其中指令可以以可能不同于程序顺序的顺序分派到执行单元。但是,指令仍然按程序顺序停用,以便程序观察到的行为与程序员直观预期的行为相同。(尽管可以设计一个OoO处理器,在一定的约束下以某种不自然的顺序停用指令。请参阅关于这个想法的基于模拟的研究:最大化有限资源:基于限制的研究和乱序提交的分类)。

推测执行是一种执行模型,在这种模型中,可以获取指令并进入管道并开始执行,而无需确定它们确实需要执行(根据程序的控制流)。该术语通常用于特指管道执行阶段的推测执行。Meltdown论文在第3页确实定义了这些术语:

在本文中,我们以更严格的含义提及推测执行,其中它指的是分支之后的指令序列,并使用术语乱序执行来指代在处理器提交所有先前指令的结果之前执行操作的任何方式。

这里的作者特别指的是在执行单元中执行超过预测分支的指令时进行分支预测。这通常是该术语的寓意。尽管可以通过使用其他技术(如值预测和推测内存消歧)来设计一个推测性地执行指令而不进行任何分支预测的处理器。这将是对数据或内存依赖关系的推测,而不是对控制的推测。一条指令可能被分派到具有不正确操作数或加载错误值的执行单元。推测也可能发生在执行资源的可用性、较早指令的延迟或内存层次结构中特定单元中所需值的存在上。

请注意,指令可以推测性地执行,但按顺序执行。当流水线的解码阶段识别出条件分支指令时,它可以推测分支及其目标,并从预测的目标位置获取指令。但是,指令也可以按顺序执行。但是,请注意,一旦推测的条件分支指令和从预测路径(或两条路径)获取的指令到达发布阶段,在所有较早的指令发出之前,它们都不会发出。英特尔Bonnell微架构是一个真正的处理器的例子,它是有序的,并支持分支预测。

设计用于执行简单任务并用于嵌入式系统或物联网设备的处理器通常既不是推测性的,也不是面向对象的。桌面和服务器处理器既是推测性的,也是面向对象的。当与面向对象一起使用时,推测执行特别有益。

当我阅读Meltdown和Spectre的论文并做额外的研究时,困惑出现了。Meltdown论文中说Meltdown是基于乱序执行的,而其他一些资源,包括关于分离执行的wiki页面,则表示Meltdown是基于推测执行的。

论文中描述的Meltdown漏洞需要推测和乱序执行。然而,这有点模糊,因为有许多不同的推测和乱序执行实现。Meltdown不适用于任何类型的OoO或推测执行。例如,ARM11(用于Raspberry Pis)支持一些有限的OoO和推测执行,但它并不容易受到攻击。

有关崩溃的更多详细信息,请参阅彼得的回答和他的其他回答。

相关:超标量和OoO执行有什么区别?。

匿名用户

我仍然很难弄清楚Meltdown如何使用推测执行。论文中的示例(我之前在这里提到的同一个)在注释中只使用IMOOoO-@Name

Meltdown基于Intel CPU乐观地推测负载不会发生故障,如果故障负载到达负载端口,则是早期错误预测分支的结果。因此,负载uop被标记,因此如果它达到退休状态,它将发生故障,但执行继续使用数据进行推测页表条目表明您不允许从用户空间读取数据。

当负载执行时,它不会触发代价高昂的异常恢复,而是等到它肯定达到退休状态,因为这是机器处理分支未命中的廉价方法-

在现代OoO流水线CPU中,所有指令在报废前都被视为推测性的。只有在报废时,指令才变得非推测性的。无序机器并不真正知道或关心它是在推测分支的一侧,该分支已被预测但尚未执行,还是推测过去可能存在故障的负载。“推测”负载不会出错或ALU指令不会引发异常,即使在并不真正被认为是推测性的CPU中也会发生,但完全无序的执行将其变成另一种推测。

我不太担心“推测执行”的确切定义,以及什么重要/什么不重要。我更感兴趣的是现代无序设计实际上是如何工作的,而且在管道结束之前甚至不尝试区分推测和非推测实际上更简单。这个答案甚至没有试图用推测性指令获取(基于分支预测)而不是执行来解决更简单的有序管道,或者介于两者之间的任何地方,以及Tomasulo的成熟算法,该算法具有ROB调度程序,带有OoO exec按顺序退休以获取精确异常。

例如,只有在停用之后,存储才能从存储缓冲区提交到L1d缓存,而不是之前。为了吸收短暂的突发和缓存未命中,它也不必作为停用的一部分发生。因此,唯一非推测性的乱序事情之一是将存储提交到L1d;就架构状态而言,它们肯定已经发生了,因此即使发生中断/异常,它们也必须完成。

如果到达故障停用机制是避免在分支错误预测的阴影下进行昂贵工作的好方法。如果异常确实触发,它还为CPU提供正确的架构状态(寄存器值等)。无论您是否让OoO机器继续根据指令搅拌到您检测到异常的点之外,您都确实需要它。

分支未命中是特殊的:在分支上有记录微架构状态(如寄存器分配)的缓冲区,因此分支恢复可以回滚到该状态,而不是刷新管道并从最后一个已知的良好退休状态重新启动。分支在实际代码中确实有相当多的错误预测。其他例外非常罕见。

现代高性能CPU可以在分支未命中之前保持(无序)执行uops,同时丢弃该点之后的uops和执行结果。快速恢复比从可能远远落后于错误预测发现点的退休状态丢弃和重新启动所有内容便宜得多。

例如,在循环中,处理循环计数器的指令可能会远远领先于循环主体的其余部分,并在最后检测到错误预测,从而足够快地重定向前端,并且可能不会损失太多实际吞吐量,特别是如果瓶颈是依赖链的延迟或uop吞吐量以外的东西。

这种优化的恢复机制仅用于分支(因为状态快照缓冲区有限),这就是为什么分支未命中与完整管道刷新相比相对便宜的原因。(例如,在Intel上,内存排序机器清除,性能计数器machine_clears。memory_ordering:在超兄弟姐妹和非超兄弟姐妹之间,生产者-消费者共享内存位置的延迟和吞吐量成本是多少?)

不过,异常并非闻所未闻;正常操作过程中确实会发生页面错误。例如,存储到只读页面会触发写时复制。加载或存储到未映射的页面会触发页面输入或处理惰性映射。但是,即使在频繁分配新内存的进程中,每个页面错误之间通常也会运行数千到数百万条指令。(1GHzCPU上每微秒或毫秒1条)。在没有映射新内存的代码中,您可以毫无例外地运行更长时间。大多数只是计时器中断,偶尔在没有I/O的纯数字运算中。

但无论如何,在您确定异常真的会触发之前,您不想触发管道刷新或任何昂贵的事情。并且您确定您有正确的异常。例如,可能早期错误加载的加载地址没有尽快准备好,因此第一个执行的错误加载不是程序顺序中的第一个。等到退休是获得精确异常的廉价方法。就处理这种情况的额外晶体管而言,这很便宜,并且让通常的有序退休机器准确地确定哪些异常触发是快速的。

在被标记为故障的退休指令之后执行指令所做的无用工作需要消耗一点点能量,并且不值得阻止,因为异常非常罕见。

这就解释了为什么首先设计容易受到Meltdown攻击的硬件是有意义的。显然,既然已经想到了Meltdown,继续这样做是不安全的。

我们不需要在错误加载后阻止推测执行;我们只需要确保它实际上没有使用敏感数据。问题不在于成功推测加载,Meltdown基于以下指令使用该数据来产生依赖于数据的微架构效果。(例如,根据数据触摸缓存行)。

因此,如果负载端口将加载的数据屏蔽为零或其他值,并设置了故障退休标志,执行将继续,但无法获得有关秘密数据的任何信息。这应该需要大约1个额外的关键路径门延迟,这在负载端口中可能是可能的,而不会限制时钟速度或增加额外的延迟周期。(1个时钟周期足够长,逻辑可以在管道级中通过许多AND/OR门传播,例如一个完整的64位加法器)。

相关:我在为什么AMD处理器不/不容易受到Meltdown和Spectre的影响?中建议了相同的机制来修复Meltdown的硬件修复。