乱序执行与推测执行

Out-of-order execution vs. speculative execution

我已阅读有关 out-of-order execution and speculative exectution 的维基百科页面。

我不明白的是相同点和不同点。在我看来,推测执行在尚未确定条件值​​时使用乱序执行。例如。

当我阅读 Meltdown 和 Spectre 的论文并进行额外的研究时,困惑就来了。 Meltdown paper that Meltdown is based on out-of-order execution, while some other resources including the wiki page about sepeculative execution状态中声明Meltdown是基于推测执行。

我想澄清一下。

推测执行和out-of-order执行是正交的。可以设计一种 OoO 但不是推测性或推测性但 in-order 的处理器。 OoO 执行是一种执行模型,在该模型中,指令可以按可能不同于程序顺序的顺序分派到执行单元。然而,这些指令仍然按程序顺序退出,因此程序观察到的行为与程序员直观预期的行为相同。 (虽然可以设计一个 OoO 处理器,它在某些约束条件下以某种不自然的顺序退出指令。请参阅 simulation-based 对此想法的研究: 最大化有限资源:Limit-Based 研究和分类 Out-of-Order 提交).

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

In this paper, we refer to speculative execution in a more restricted meaning, where it refers to an instruction sequence following a branch, and use the term out-of-order execution to refer to any way of getting an operation executed before the processor has committed the results of all prior instructions.

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

请注意,可以推测性地执行指令,但 in-order。当流水线的解码阶段识别出条件分支指令时,它可以推测分支及其目标并从预测的目标位置获取指令。但是,仍然可以执行指令 in-order。但是,请注意,一旦推测的条件分支指令和从预测路径(或两条路径)中提取的指令到达发布阶段,将发布其中的 none 直到所有更早的指令都发布。 Intel Bonnell 微架构是 in-order 并支持分支预测的真实处理器的示例。

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

The confusion came when I read the papers of Meltdown and Spectre and did additional research. It is stated in the Meltdown paper that Meltdown is based on out-of-order execution, while some other resources including the wiki page about sepeculative execution state that Meltdown is based on speculative execution.

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

有关 Meltdown 和他的其他 answer 的更多详细信息,请参阅 Peter 的回答。

相关:What is the difference between Superscalar and OoO execution?.

I'm still having hard time figuring out, how Meltdown uses speculative execution. The example in the paper (the same one I mentioned here earlier) uses IMO only OoO -

Meltdown 是基于英特尔 CPU 乐观地 推测 负载不会发生故障,并且如果故障负载到达负载端口,那是早期错误预测分支的结果。所以加载 uop 被标记,所以如果它到达退休就会出错,但是执行继续推测性地 使用数据页面 table 条目说你不允许从 user-space[= 读取68=].

它不会在加载执行时触发代价高昂的 exception-recovery,而是等到它确实达到退休状态,因为这是机器处理分支未命中 -> 不良负载情况的一种廉价方式。在硬件中,管道保持管道更容易,除非您需要它停止/停止以确保正确性。例如根本没有 page-table 条目的加载,因此 TLB 未命中,必须等待。但即使在 TLB hit 上等待(对于具有阻止使用它的权限的条目)也会增加复杂性。通常 page-fault 只会在页面遍历失败(找不到虚拟地址的条目)之后,或者在加载或存储失败时引发它命中的 TLB 条目的权限。

在现代 OoO 流水线 CPU 中,所有 指令在退休前都被视为推测性的 。只有在退休时,指示才会变成 non-speculative。 Out-of-Order 机器并不真正知道或关心它是在推测已预测但尚未执行的分支的一侧,还是在推测过去的 potentially-faulting 负载。 "Speculating" 加载不会出错或 ALU 指令不会引发异常 ,但完全 out-of-order 执行会将其变成另一种推测。

我不太担心 "speculative execution" 的确切定义,以及什么重要/什么不重要。我对现代 out-of-order 设计的实际工作方式更感兴趣,而且在管道结束之前甚至不尝试区分投机和 non-speculative 实际上更简单。这个答案甚至没有试图解决更简单的 in-order 具有推测性 instruction-fetch 的管道(基于分支预测)但不是执行,或者在 OoO exec + [=] 和 full-blown Tomasulo's algorithm with a ROB + scheduler 之间的任何地方95=] 精确例外退休。

例如,只有 退休后,存储才能从存储缓冲区提交到 L1d 缓存,而不是之前。为了吸收短暂的突发和缓存未命中,它也不必作为退休的一部分发生。因此,仅有的 non-speculative out-of-order 事情之一就是将商店提交给 L1d;就架构状态而言,它们肯定已经发生,所以即使发生中断/异常,它们也必须完成。

fault-if-reaching-retirement 机制是避免在分支预测错误的阴影下进行昂贵工作的好方法。如果异常确实触发,它还会为 CPU 提供正确的体系结构状态(寄存器值等)。你确实需要它,无论你是否让 OoO 机器在你检测到异常的地方继续搅动指令。


Branch-misses比较特殊:有记录-架构状态的缓冲区(如register-allocation)在分支上,因此 branch-recovery 可以回滚到那个状态,而不是刷新管道并从最后一个 known-good 退休状态重新启动。分支确实在实际代码中错误预测了相当多的数量。其他例外情况非常罕见。

现代 high-performance CPUs 可以保持 (out-of-order) 在分支未命中之前执行微指令,同时丢弃该点之后的微指令和执行结果。快速恢复比从可能远远落后于发现错误预测点的退休状态丢弃并重新启动一切要便宜得多。

例如在一个循环中,处理循环计数器的指令可能会远远领先于循环体的其余部分,并很快检测到末尾的错误预测以重定向 front-end 并且可能不会损失太多实际吞吐量,尤其是如果瓶颈是依赖链的延迟或 uop 吞吐量以外的东西。

这种优化的恢复机制仅用于分支(因为 state-snapshot 缓冲区是有限的),这就是为什么分支未命中与完整管道刷新相比相对便宜的原因。 (例如,在 Intel 上,memory-ordering 机器清除,性能计数器 machine_clears.memory_ordering


不过 unheard-of 没有例外; page-faults 确实发生在正常操作过程中。例如存储到 read-only 页面触发 copy-on-write。加载或存储到未映射的页面触发 page-in 或处理惰性映射。但是,即使在频繁分配新内存的进程中,每次页面错误之间通常也会有数千到数百万条指令 运行 。 (在 1GHz CPU 上每个微控制器 1 个或 milli-second)。在不映射新内存的代码中,您可以毫无例外地走得更远。大多数只是偶尔在纯数字 c运行ching 中没有 I/O.

的定时器中断

但无论如何,您不想触发管道冲洗或任何昂贵的东西,直到你确定异常会真正触发。并且您确定您有 right 例外。例如也许早期故障加载的加载地址没有尽快准备好,所以第一个执行的故障加载不是程序顺序中的第一个。等到退休是获得精确异常的一种廉价方法。就处理这种情况的附加晶体管而言成本低廉,并且让通常的 in-order 退休机制快速准确地找出触发的异常。

在一条被标记为错误的指令退出时执行指令后执行指令的无用工作会消耗一点点能量,并且不值得阻塞,因为异常非常罕见。

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


廉价修复 Meltdown

我们不需要在加载错误后阻止推测执行;我们只需要确保它实际上不使用敏感数据。这不是推测性的加载成功问题,Meltdown 是基于以下说明使用该数据产生 data-dependent 微架构效果。 (例如,根据数据访问缓存行)。

因此,如果加载端口将加载的数据屏蔽为零或其他内容以及设置 fault-on-retirement 标志,执行将继续但无法获得有关秘密数据的任何信息。这应该需要大约 1 个关键路径的额外门延迟,这在负载端口中可能是可能的,而不会限制时钟速度或添加额外的延迟周期。 (1 个时钟周期足以让逻辑通过流水线级内的许多 AND/OR 门传播,例如完整的 64 位加法器)。

相关:我在 Why are AMD processors not/less vulnerable to Meltdown and Spectre?.

中建议使用相同的机制来修复 Meltdown 的硬件