推测执行的 CPU 分支是否可以包含访问 RAM 的操作码?

gol*_*sky 4 cpu cpu-architecture speculative-execution

据我了解,当 CPU 推测性地执行一段代码时,它会在切换到推测性分支之前“备份”寄存器状态,以便如果预测结果错误(使分支无用)——寄存器状态将是安全恢复,而不会破坏“状态”。

所以,我的问题是:推测执行的 CPU 分支是否可以包含访问 RAM 的操作码?

我的意思是,访问 RAM 不是“原子”操作——如果数据当前不在 CPU 缓存中,那么从内存中读取一个简单的操作码可能会导致实际的 RAM 访问,这可能会变成一个非常耗时的操作,从 CPU 的角度来看。

如果在推测分支中确实允许这种访问,它是否仅用于读取操作?因为,我只能假设,如果一个分支被丢弃并执行“回滚”,根据它的大小恢复写操作可能会变得非常缓慢和棘手。而且,可以肯定的是,至少在某种程度上支持读/写操作,因为寄存器本身,在某些 CPU 上,据我所知,物理上位于 CPU 缓存上。

所以,也许更精确的表述是:推测执行的一段代码有什么限制?

Pet*_*des 13

推测性乱序 (OoO) 执行的主要规则是:

  1. 保留指令按程序顺序依次运行的错觉
  2. 确保推测包含在检测到错误推测时可以回滚的事物中,并且其他内核无法观察到其持有错误值。物理寄存器,后端本身跟踪指令顺序是的,但不是缓存。缓存与其他核心一致,因此存储必须在非推测性之后才提交缓存。

OoO exec 通常通过将所有内容视为投机性直到退休来实现。每个加载或存储都可能出错,每个 FP 指令都可能引发 FP 异常。分支是特殊的(与异常相比)只是因为分支错误预测并不罕见,因此处理分支未命中的早期检测和回滚的特殊机制是有帮助的。


是的,可缓存加载可以推测性和 OoO 执行,因为它们没有副作用。

由于存储缓冲区,存储指令也可以推测性地执行。 存储的实际执行只是将地址和数据写入存储缓冲区。 (相关:英特尔硬件上存储缓冲区的大小?究竟什么是存储缓冲区?比这更具技术性,具有更多的 x86 焦点。我认为这个答案适用于大多数 ISA。)

提交到 L1d 缓存发生在存储指令从 ROB 退出的一段时间,即当已知存储是非推测性的时,相关的存储缓冲区条目“毕业”并有资格提交缓存并变得全局可见。存储缓冲区将执行与其他内核可以看到的任何内容分离,并将该内核与缓存未命中存储隔离,因此即使在有序 CPU 上,它也是一个非常有用的功能。

在存储缓冲区条目“毕业”之前,当回滚错误推测时,它可以与指向它的 ROB 条目一起被丢弃。

(这就是为什么即使是强排序的硬件内存模型仍然允许 StoreLoad 重新排序https://preshing.com/20120930/weak-vs-strong-memory-models/ - 这对于良好的性能几乎是必不可少的,不要让后面的加载等待更早存储实际提交。)

存储缓冲区实际上是一个循环缓冲区:由前端分配的条目(在分配/重命名管道阶段)并在将存储提交到 L1d 缓存时释放。(通过MESI与其他内核保持一致)。

像 x86 这样的强排序内存模型可以通过按顺序从存储缓冲区提交到 L1d 来实现。条目是按程序顺序分配的,因此存储缓冲区基本上可以是硬件中的循环缓冲区。如果存储缓冲区的头部用于尚未准备好的缓存行,则弱序 ISA 可以查看较年轻的条目。

一些国际检索单位(尤其是弱有序)也做合并存储缓冲项,以建立一个单一的8字节的承诺L1D出对32位存储的,例如


读取可缓存的内存区域被假定为没有副作用,并且可以通过 OoO exec、硬件预取或任何其他方式推测性地完成。错误推测可以“污染”缓存并通过接触真正执行路径不会的缓存行来浪费一些带宽(甚至可能触发 TLB 未命中的推测性页面遍历),但这是唯一的缺点1

MMIO 区域(其中读取确实有副作用,例如使网卡或 SATA 控制器执行某些操作)需要标记为不可缓存,以便 CPU 知道不允许从该物理地址进行推测性读取。 如果您弄错了,您的系统将不稳定- 我的回答涵盖了您针对推测负载询问的许多相同细节。

高性能 CPU 具有包含多个条目的负载缓冲区,用于跟踪动态负载,包括在 L1d 缓存中未命中的负载。(即使在有序 CPU 上也允许命中未命中和未命中未命中,仅当/当指令尝试读取尚未准备好的加载结果寄存器时才停止)。

在 OoO exec CPU 中,当一个加载地址在另一个加载地址之前准备好时,它还允许 OoO exec。当数据最终到达时,等待来自加载结果的输入的指令准备运行(如果它们的其他输入也准备好)。因此,加载缓冲区条目必须连接到调度程序(在某些 CPU 中称为保留站)。

另请参阅关于 RIDL 漏洞和负载“重放”,了解有关英特尔 CPU 如何专门处理等待的 uops的更多信息,当数据可能从 L2 到达 L2 命中时,积极尝试启动它们。


脚注 1:这个缺点,结合用于检测/读取微架构状态(高速缓存线热或冷)到架构状态(寄存器值)的定时侧通道是启用 Spectre 的原因。( https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)#Mechanism )

了解 Meltdown 对于了解英特尔 CPU 如何选择处理错误抑制的推测性负载的细节非常有用。 http://blog.stuffedcow.net/2018/05/meltdown-microarchitecture/


而且,可以肯定的是,支持读/写操作

是的,如果您谈论的是解码为指令 uops 的现代 x86,则通过将它们解码以在逻辑上分离加载/ALU/存储操作。加载像正常加载一样工作,存储将 ALU 结果放入存储缓冲区。所有 3 个操作都可以由乱序后端正常安排,就像您编写单独的指令一样。

如果您的意思是原子RMW,那么这真的不是推测性的。缓存是全局可见的(共享请求可以随时出现)并且无法回滚它(嗯,除了英特尔为事务内存所做的任何事情......)。您绝不能在缓存中放入错误的值。请参阅“int num”的 num++ 可以是原子的吗?有关如何处理原子 RMW 的更多信息,尤其是在现代 x86 上,通过延迟响应以在加载和存储提交之间共享/无效该行的请求。

然而,这并不意味着lock add [rdi], eax序列化整个管道:加载和存储是唯一被重新排序的指令吗?表明其他独立指令的推测性 OoO exec可能发生在原子 RMW 周围。(相对于像lfence这样的 exec 屏障会耗尽 ROB 的情况)。

许多RISC ISA仅通过加载链接/存储条件指令提供原子RMW ,而不是单个原子RMW指令。

[读/写操作...],至少在某种程度上,由于寄存器本身,在某些 CPU 上,据我所知,物理上位于 CPU 缓存上。

嗯?错误的前提,这个逻辑没有意义。缓存必须始终正确,因为另一个核心可能随时要求您共享它。与此核心私有的寄存器不同。

寄存器文件像缓存一样由 SRAM 构建,但它们是分开的。板上有一些带有 SRAM存储器(不是高速缓存)的微控制器,并且使用该空间的早期字节对寄存器进行存储器映射。(例如 AVR)。但这些似乎都与乱序执行无关;缓存内存的缓存线绝对不同于用于完全不同的东西的缓存线,例如保存寄存器值。

花费晶体管预算来进行推测执行的高性能 CPU 将缓存与寄存器文件结合起来也不太可能。然后他们会竞争读/写端口。一个具有总读和写端口的大缓存比一个小的快速寄存器文件(许多读/写端口)和一个小的(如 32kiB)L1d 缓存和几个读端口和 1 个写要贵得多(面积和功率)港口。出于同样的原因,我们使用分离式 L1 缓存,并拥有多级缓存,而不是现代 CPU 中每个核心只有一个大型私有缓存。为什么大部分处理器的L1缓存比L2缓存小?


相关阅读/背景


  • https://en.wikipedia.org/wiki/Memory_disambiguation - CPU 如何处理从存储缓冲区到负载的转发,或者如果存储实际上比此负载更年轻(按程序顺序晚),则不处理。
  • https://blog.stuffedcow.net/2014/01/x86-memory-disambiguation/ - x86 处理器中的存储到负载转发和内存消歧。非常详细的存储转发测试结果和技术讨论,包括来自与存储不同部分重叠的窄负载,以及靠近缓存线边界的负载。(https://agner.org/optimize/在他的微架构 PDF 中有一些更容易理解但不太详细的信息,关于存储转发是慢还是快。)
  • 全局不可见的加载指令- 从与最近存储部分重叠的加载中进行存储转发,部分不给我们一个角落案例,它揭示了 CPU 的工作方式,以及它如何/没有意义考虑内存(排序) 楷模。请注意,C++ std::atomic 无法创建执行此操作的代码,尽管 C++20 std::atomic_ref 可以让您执行对齐的 4 字节原子存储,该存储与对齐的 8 字节原子加载重叠。

  • 感谢您提供信息丰富且详细的答案。 (2认同)
  • 哇,多么好的答案啊! (2认同)
  • @玛格丽特布鲁姆:谢谢。我之前写过一些答案,我打算解释什么是存储缓冲区以及它的用途,但他们最终陷入了具体细节的泥潭,并且很快就掌握了超级技术。我想这次我成功地写了一篇对初学者更友好的相关概念的实际介绍。 (2认同)
  • 典型的好答案。缓存可以包含推测状态;硬件事务内存可以通过允许推测性写入缓存并且不让其他代理可见来实现。然而,将一个已经很复杂的概念复杂化可能并不明智。理论上,更离奇的是,MMIO 访问可以被缓存,尽管保证正确行为的复杂性会限制这种行为的总回报(许多 I/O 读取没有副作用,甚至一些写入也是安全的,类似到一些推测性堆栈/TLS 写入)。缓存MMIO更是“不必要的复杂化”。 (2认同)

归档时间:

查看次数:

518 次

最近记录:

4 年,11 月 前