14

我已经阅读了关于乱序执行推测执行的维基百科页面。

我无法理解的是相同点和不同点。例如,在我看来,推测执行在尚未确定条件的值时使用了乱序执行。

当我阅读 Meltdown 和 Spectre 的论文并进行更多研究时,困惑就来了。Meltdown 论文中指出,Meltdown 是基于乱序执行的,而其他一些资源,包括关于投机执行的 wiki 页面表明 Meltdown 是基于投机执行。

我想澄清一下。

4

2 回答 2

18

推测执行和乱序执行是正交的。可以设计一种 OoO 但不是投机或投机但有序的处理器。OoO 执行是一种执行模型,其中指令可以按可能不同于程序顺序的顺序分派到执行单元。然而,指令仍然按程序顺序退出,因此程序观察到的行为与程序员直观预期的行为相同。(尽管可以设计一个 OoO 处理器,它以某种不自然的顺序在某些约束下退出指令。请参阅关于此想法的基于模拟的研究:Maximizing Limited Resources: a Limit-Based Study and Taxonomy of Out-of-Order Commit)。

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

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

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

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

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

当我阅读 Meltdown 和 Spectre 的论文并进行更多研究时,困惑就来了。Meltdown 论文中指出,Meltdown 是基于乱序执行的,而其他一些资源,包括关于投机执行的 wiki 页面,表明 Meltdown 是基于投机执行。

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

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

相关:Superscalar 和 OoO 执行有什么区别?.

于 2018-04-01T22:35:04.193 回答
11

我仍然很难弄清楚 Meltdown 如何使用投机执行。论文中的示例(与我之前提到的相同)在评论中仅使用 IMO OoO - @Name

Meltdown 基于 Intel CPU 乐观地推测负载不会出现故障,并且如果出现故障的负载到达负载端口,则它是早期错误预测分支的结果。因此,加载 uop 被标记,因此如果它达到退休状态,它将出现故障,但是使用页表条目说您不允许从 user-space 读取的数据推测性地继续执行。

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

在现代 OoO 流水线 CPU 中,所有指令都被视为推测性的,直到退休。只有在退休时,指令才会变成非投机性的。Out-of-Order 机器并不真正知道或关心它是在推测已预测但尚未执行的分支的一侧,还是推测过去可能出现故障的负载。即使在实际上不被认为是推测性的 CPU 中, “推测”加载不会出错或 ALU 指令不会引发异常,但完全无序执行会将其变成另一种推测。

我不太担心“投机执行”的确切定义,什么重要/什么不重要。我对现代无序设计的实际工作方式更感兴趣,而且在管道结束之前甚至不尝试区分投机和非投机实际上更简单。这个答案甚至没有尝试通过推测性指令获取(基于分支预测)而不是执行来解决更简单的有序流水线,或者在这与成熟的 Tomasulo 算法与 ROB + 调度程序和 OoO exec + in之间的任何地方-为了精确的例外而退休。

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

fault-if-reaching-retirement 机制是避免在分支错误预测的阴影下进行昂贵工作的好方法。如果异常触发,它还会为 CPU 提供正确的架构状态(寄存器值等)。无论您是否让 OoO 机器在您检测到异常的点之外继续处理指令,您都需要这样做。


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

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

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

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


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

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

在标记为退出故障的指令之后执行指令的无用工作会消耗一点功率,并且不值得阻止,因为异常非常罕见。

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


廉价修复 Meltdown

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

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

相关:我在为什么 AMD 处理器不容易受到 Meltdown 和 Spectre 的影响?.

于 2018-04-04T22:07:27.593 回答