【问题标题】:Out-of-order execution vs. speculative execution乱序执行与投机执行
【发布时间】:2018-09-11 02:49:09
【问题描述】:

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

但我无法理解的是相同点和不同点。在我看来,推测执行在没有确定条件值​​的情况下使用乱序执行。

当我阅读 Meltdown 和 Spectre 的论文并进行额外研究时,困惑出现了。 Meltdown paper 中指出 Meltdown 是基于乱序执行的,而其他一些资源(包括关于 sepeculative execution 的 wiki 页面)指出 Meltdown 是基于推测执行。

我想澄清一下。

【问题讨论】:

  • 对我来说,乱序执行是推测执行的一种形式(推测正在进行的 insts 不会产生与新 insts 相关的副作用) . spec exec 的另一种形式是分支预测,另一种是提前遍历页表。从技术上讲,OOO 是在不同依赖链之间移动执行的能力,因此跳过一个 inst 并移动到下一个;但这是打赌旧的机构不会出错,例如,因此是一种猜测。

标签: cpu-architecture speculative-execution


【解决方案1】:

推测执行和乱序执行是正交的。可以设计一种 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 和他的其他 answer 的更多详细信息,请参阅 Peter 的回答。

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

【讨论】:

  • OoO exec 在没有推测的情况下如何工作?指令需要等待较早的独立加载/存储已知无故障,即使数据尚未准备好(例如等待 TLB 命中,但不是缓存未命中)? ARM除法指令即使在除以零IIRC时也不会出错,因此至少它不必为此停止。 (我想我们在某个时候讨论过这个问题,但我忘记了你的答案是什么。)
  • 另外,请注意,对于有序管道,推测 fetch / decode 不会让推测到达 exec 阶段,因此错误预测分支之后的指令实际上永远不会得到执行。将这种推测性的执行称为有点乐观。 (除非你的意思是分支指令的执行可以按顺序开始,但很长一段时间都没有完成,所以后面的一些指令有机会执行)
  • (更新:好的,是的,那个编辑对我来说更有意义,不再暗示非投机性的 OoO exec。)
【解决方案2】:

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

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

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

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

我不太担心“投机执行”的确切定义,以及什么重要/什么不重要。我对现代无序设计的实际工作方式更感兴趣,而且在管道结束之前甚至不尝试区分投机和非投机实际上更简单。这个答案甚至没有尝试通过推测性指令获取(基于分支预测)而不是执行来解决更简单的有序管道,或者在这之间的任何地方和full-blown Tomasulo's algorithm with a ROB + scheduler 与 OoO exec + 有序退休以获取精确异常。

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

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


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

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

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

这种优化的恢复机制仅用于分支(因为状态快照缓冲区是有限的),这就是为什么分支未命中与完整管道刷新相比相对便宜的原因。 (例如在 Intel 上,内存排序机器清除,性能计数器 machine_clears.memory_ordering: What are the latency and throughput costs of producer-consumer sharing of a memory location between hyper-siblings versus non-hyper siblings?


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

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

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

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


廉价修复 Meltdown

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

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

相关:我在 Why are AMD processors not/less vulnerable to Meltdown and Spectre? 中为 Meltdown 的硬件修复建议了相同的机制。

【讨论】:

  • @KrazyGlew:知道如果有任何类型的 TLB 命中,即使是权限不足的情况,你会从加载中获得什么样的微架构优势?如果early P6 before Conroe/Merom didn't do it,我想它没有我想象的那么明显或重要。
  • 我确定我理解这一点,你的意思是问题是即使TLB在查看PTE后发现我们不允许访问它,数据也会被带到缓存中?
  • @abjoshi:在 Meltdown 中,问题是 L1d 中已经很热的线路可以报告命中并为后续指令的推测执行提供实际数据,即使 TLB 条目只显示主管(内核模式)代码应该被允许读取这个页面。将新行带入缓存的推测性访问是对我们允许读取的数组。 (稍后将非推测性地读取缓存时序侧通道,以将该微架构状态转换为架构状态 - 非推测性代码中寄存器中的一个数字。)
  • 有关 Meltdown 工作原理的更多详细信息,请参阅 blog.stuffedcow.net/2018/05/meltdown-microarchitecture 我不确定 Meltdown 是否适用于“秘密”数据的缓存未命中问题。
  • 感谢您的链接。所以你的意思是该行必须已经在 L1D 缓存中?另外,我猜缓存未命中有两种情况: 1. 页面映射到 TLB 2. 页面未映射,因此 TLB 未命中。在任何一种情况下,TLB 最终都会找到 pte 并假设我们不允许访问它,并通知内核将指令标记为异常。我的困惑是,在这两种情况下,数据是否真的被带入了缓存,如果是,谁将请求发送到内存、MMU 或缓存控制器?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-04
  • 2019-01-09
  • 1970-01-01
  • 2019-12-12
  • 2019-02-19
相关资源
最近更新 更多