4

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

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

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

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

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

4

1 回答 1

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 可以查看较年轻的条目。

一些 ISA(尤其是弱排序)也会合并存储缓冲区条目,以从一对 32 位存储中创建单个 8 字节提交到 L1d,例如.


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

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

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

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

另请参阅关于 RIDL 漏洞和负载的“重放”,了解更多关于英特尔 CPU 如何专门处理正在等待的微指令,方法是在数据可能从 L2 到达 L2 命中的周期中积极尝试启动它们。


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

了解 Meltdown 对于了解 Intel 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 将缓存与寄存器文件结合起来也不太合理。然后他们会竞争读/写端口。一个具有总读写端口的大型缓存(面积和功率)比一个微小的快速寄存器文件(许多读/写端口)和一个具有几个读端口和 1 个写端口的小型(如 32kiB)L1d 缓存要昂贵得多港口。出于同样的原因,我们使用拆分 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/在他的 microarch PDF 中有一些更易于理解但不太详细的关于存储转发何时慢与快的信息。)
  • 全局不可见的加载指令- 从与最近的存储部分重叠的加载中进行存储转发,并且部分不会给我们提供一个极端案例,它可以阐明 CPU 的工作方式,以及考虑内存的方式/没有意义(排序) 楷模。请注意,C++ std::atomic 无法创建执行此操作的代码,尽管 C++20 std::atomic_ref 可以让您执行与对齐的 8 字节原子加载重叠的对齐 4 字节原子存储。
于 2020-10-01T02:46:33.937 回答