乱序执行vs.预测执行

16

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

然而,我不太理解它们之间的相似性和差异。对我来说,当它没有确定条件的值时,预测执行使用乱序执行。

混淆出现在我阅读Meltdown和Spectre的论文以及做进一步研究时。在Meltdown论文中指出Meltdown是基于乱序执行的,而一些其他资源包括维基百科关于预测执行的页面则指出Meltdown是基于预测执行的。

我希望能够澄清这一点。


对我来说,乱序执行是一种推测执行的形式(推测正在执行的指令不会对新指令产生相关的副作用)。另一种推测执行的形式是分支预测,另一种是提前遍历页表。从技术上讲,乱序执行是在不同依赖链之间移动执行的能力,因此跳过一个指令并转到下一个指令;但这是打赌旧指令不会出错,例如,因此是一种推测。 - Margaret Bloom
2个回答

25
“Speculative execution和out-of-order execution是正交的。可以设计一种处理器,它是OoO但不是speculative或者是speculative但是in-order。OoO执行是一种执行模型,其中指令可以按照可能与程序顺序不同的顺序分派到执行单元。然而,指令仍然按照程序顺序退役,以便程序员预期的观察行为与实际表现相同。(虽然可以设计一个OoO处理器,以某些约束条件下以某种不自然的顺序退役指令。请参见这个想法的基于模拟的研究:Maximizing Limited Resources: a Limit-Based Study and Taxonomy of Out-of-Order Commit)。

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

在本文中,我们将“投机执行”解释为一种更加狭义的意义,即指在分支之后遵循的指令序列,并使用“乱序执行”这个术语来指代在处理器提交所有先前指令的结果之前执行操作的任何方式。作者在此特别指出了分支预测与执行预测分支后的指令序列在执行单元中的情况。虽然可以通过使用其他技术(如值预测和投机性内存消除)设计一个不需要任何分支预测的投机执行指令的处理器,但这将是对数据或内存依赖关系而非控制进行的投机。指令可能会被调度到一个具有不正确操作数或加载错误值的执行单元中。投机也可能发生在执行资源的可用性上,早期指令的延迟上,或者内存层次结构中特定单元中所需值的存在上。
请注意,指令可以被投机执行但仍然保持按顺序执行。当流水线的解码阶段识别到条件分支指令时,它可以对分支及其目标进行猜测,并从预测的目标位置获取指令。但是,指令也可以按顺序执行。然而,需要注意的是,一旦猜测的条件分支指令和从预测的路径(或两个路径)获取的指令达到发射阶段,除非所有先前的指令都已发射,否则它们都不会被发射。英特尔 Bonnell 微架构是一个实际处理器的例子,它是按顺序执行并支持分支预测。
设计用于执行简单任务并用于嵌入式系统或物联网设备的处理器通常既不具有投机执行也不具有乱序执行。桌面和服务器处理器既具有投机执行又具有乱序执行。当与乱序执行结合使用时,投机执行特别有益。
引起混淆的原因是我在阅读 Meltdown 和 Spectre 论文以及进行额外研究时才开始的。Meltdown 论文中指出,Meltdown 是基于乱序执行的,而一些其他资源,包括关于投机执行状态的维基页面,则认为 Meltdown 是基于投机执行的。

在该论文中描述的Meltdown漏洞需要同时运用到推测执行和乱序执行。然而,这个说法有些模糊,因为有许多不同的推测执行和乱序执行实现方式。Meltdown并非与任何类型的乱序或推测执行都兼容。例如,ARM11(用于树莓派)支持一些有限的乱序和推测执行,但它并不容易受到攻击。

请参阅彼得的答案以获取有关Meltdown及其其他答案的更多详细信息。

相关:超标量和乱序执行之间有什么区别?


1
乱序执行如何在没有猜测的情况下工作?指令需要等待早期独立的加载/存储操作被确认为非故障,即使数据尚未准备好(例如,等待TLB命中,但不等待缓存未命中)?ARM除法指令即使在除以零时也不会出错,所以至少不必因此而停顿。(我记得我们曾经讨论过这个问题,但我忘记了你的答案。) - Peter Cordes
另外,请注意,在顺序流水线中,推测性的取指/译码不会让推测达到执行阶段,因此在错误预测分支后的指令实际上永远不会被执行。称之为推测性执行似乎有点过于乐观。(除非你的意思是分支指令的执行可以按顺序开始,但需要很长时间才能完成,以便一些后续指令有机会执行) - Peter Cordes
1
(更新:好的,是的,那个编辑对我来说更有意义,不再建议非推测性OoO执行。) - Peter Cordes

12
我仍然很难理解Meltdown如何使用预测执行。论文中的例子(我先前提到过的同一个例子)只使用了乱序执行 - @Name in a comment
Meltdown基于Intel CPU的乐观假设,即负载不会故障,并且如果一个故障负载达到负载端口,则是早期错误分支的结果。因此,加载uop被标记为如果到达退役将出现问题,但执行继续使用数据页面表项说您不允许从用户空间读取。
与其在负载执行时触发昂贵的异常恢复,不如等待直到它明确地到达退役,因为这对机器来说是处理分支失误 ->坏负载案例的一种廉价方式。在硬件中,使管道保持连续传输比需要停止/暂停以实现正确性更容易。例如,根本没有页表项的负载,因此TLB不命中必须等待。但即使在TLB命中(对于具有阻塞使用权限的条目),也等待会增加复杂性。通常,在失败的页面漫游(未找到虚拟地址的条目)或在未通过TLB条目的权限的加载或存储退役后才会引发页面错误。
在现代的乱序流水线CPU中,直到退役前,所有指令都被视为投机性。乱序执行机制实际上并不知道或关心它是否正在沿着预测但尚未执行的分支的一侧进行推测,或者正在推测过去潜在故障的负载。即使在不被认为是具有投机性的CPU中,也会“推测”负载不会故障或ALU指令不会引发异常,但完全的乱序执行将其转变为另一种推测方式。
我不太担心“推测执行”的确切定义以及什么算作/不算作。我更感兴趣的是现代乱序设计如何实际工作,而且甚至不尝试在管道的末端区分投机性和非投机性是更简单的方法。这个答案甚至没有试图解决基于分支预测的投机指令获取(而不是执行)或介于此之间的任何东西,以及带有ROB +调度程序的完整的Tomasulo算法,其中OoO exec +顺序退役用于精确异常。
例如,只有在退休之后,商店才能从存储缓冲区提交到L1d缓存,而不是之前。为了吸收短暂的突发和缓存未命中,它也不必作为退休的一部分发生。因此,唯一的非推测性乱序事情之一是将商店提交到L1d;就体系结构状态而言,它们已经发生,因此即使发生中断/异常,它们也必须完成。
达到退休时出现故障的机制是避免在分支错误预测的阴影下进行昂贵工作的好方法。如果异常确实发生,则还可以为CPU提供正确的体系结构状态(寄存器值等)。无论您是否让OoO机器继续在检测到异常的点之外继续运行指令,都需要这样做。
分支缺失很特殊:有一些缓冲区记录了分支上的微架构状态(如寄存器分配),因此分支恢复可以回滚到该状态,而不是刷新流水线并从最后一个已知的退役状态重新启动。在实际代码中,分支确实会出现相当多的错误预测。其他异常情况非常罕见。
现代高性能CPU可以保持(乱序)执行分支缺失之前的uops,同时丢弃之后的uops和执行结果。快速恢复比丢弃并从可能远远落后于发现错误预测点的退役状态重新启动所有内容要便宜得多。
例如,在循环中,处理循环计数器的指令可能会超前于其余循环体,并足够快地检测到末尾的错误预测,以重定向前端,并且可能不会失去太多实际吞吐量,特别是如果瓶颈是依赖链的延迟或其他uop吞吐量之外的东西。
这种优化的恢复机制仅用于分支(因为状态快照缓冲区有限),这就是为什么与完全管道刷新相比,分支缺失相对较便宜的原因。(例如,在Intel上,内存排序机器清除,性能计数器machine_clears.memory_ordering:在超线程和非超线程之间共享内存位置的生产者-消费者共享的延迟和吞吐成本是多少?)
异常情况并不罕见,例如在正常操作过程中会发生页面故障。例如,将数据存储到只读页面会触发写时复制。加载或存储未映射页面会触发页面传入或处理懒惰映射。但即使在频繁分配新内存的进程中,每个页面故障之间通常也会运行数千到数百万条指令(在1GHz CPU上每微秒或毫秒1次)。在不映射新内存的代码中,您可以更长时间地避免异常情况。在没有I/O的纯数字计算中,通常只有定时器中断偶尔会出现。

但无论如何,在确信异常情况确实会发生之前,您都不希望触发流水线刷新或任何昂贵的操作。并且您要确信您拥有正确的异常情况。例如,可能早期故障加载的加载地址还没有准备好,因此第一个执行故障加载的不一定是程序顺序中的第一个。等到退役是获得精确异常情况的一种廉价方法。从处理这种情况所需的额外晶体管来看,这种方法是便宜的,并且让通常的按顺序退役机制确定哪个异常情况发生是快速的。

在标记为退役时出现故障的指令执行后执行的无用工作会消耗少量功率,并且不值得阻塞,因为异常情况非常罕见。

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


廉价修复Meltdown漏洞

我们不需要在故障负载之后阻止推测执行;我们只需要确保它实际上不使用敏感数据。问题不在于成功的推测负载,而是基于以下指令使用该数据来产生数据相关的微架构效应的Meltdown漏洞。(例如,基于数据触摸高速缓存线)。

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

相关:我建议在为什么AMD处理器不/较不容易受到Meltdown和Spectre攻击?中使用相同的机制来修复Meltdown的硬件漏洞。


只是为了确保我理解正确,您的意思是问题在于即使TLB查看PTE后发现我们无权访问数据,数据仍会被带到缓存中? - abjoshi - Reinstate Monica
@abjoshi:在Meltdown中,问题在于L1d中的一个已经热的行可以报告命中并提供实际数据以用于后续指令的推测执行,即使TLB条目仅表示只有监管程序(内核模式)代码才允许读取此页面。带来新行进入缓存的推测访问是针对我们允许读取的数组进行的。(并且稍后将以非推测代码的缓存时间侧信道读取该微架构状态以将其转换为体系结构状态-即寄存器中的数字。) - Peter Cordes
请参阅http://blog.stuffedcow.net/2018/05/meltdown-microarchitecture/以获取有关Meltdown如何工作的更多详细信息。我不确定Meltdown是否适用于“秘密”数据上的缓存未命中。 - Peter Cordes
谢谢提供链接。你的意思是说这行代码必须已经在L1D缓存中了吗?此外,我猜想在缓存未命中的情况下有两种情况:
  1. 页面被映射到了TLB中
  2. 页面没有映射,因此出现了TLB未命中。 在任何一种情况下,TLB最终会找到pte,并假设我们无权访问它,则通知核心标记异常指令。我的困惑是,无论哪种情况下,数据实际上是否被带入缓存,如果是,是谁发送请求到内存的,是MMU还是缓存控制器?
- abjoshi - Reinstate Monica
@abjoshi: 我不确定。我不知道第一次尝试是否会在某些微架构上填充缓存行,然后第二次尝试Meltdown攻击将让您实际读取它。如果Henry的博客在某个地方提到了这一点,我不会感到惊讶,但我没有彻底浏览过它。 - Peter Cordes
显示剩余4条评论

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