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

Can a speculatively executed CPU branch contain opcodes that access RAM?

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

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

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

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

因此,也许更精确的表述是:推测执行的代码段有哪些限制?

推测性 out-of-order (OoO) 执行的基本规则是:

  1. 按程序顺序运行保留指令的错觉
  2. 确保推测包含在检测到 mis-speculation 时可以回滚的内容,并且其他核心无法观察到这些内容持有错误的值。物理寄存器,back-end 本身跟踪指令顺序是,但不是缓存。缓存与其他核心一致,因此存储在 non-speculative.
  3. 之前不得提交缓存

OoO exec 通常通过将 一切 视为投机直到退休来实现。每个加载或存储都可能出错,每个 FP 指令都可能引发 FP 异常。分支的特殊之处(与异常相比)仅在于分支错误预测并不少见,因此处理 的特殊机制很有帮助。


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

由于存储缓冲区,存储指令也可以推测性地执行。 存储的实际执行只是将地址和数据写入存储缓冲区。(相关:比这更技术化,更关注 x86。这个答案是我认为适用于大多数 ISA。)

提交到 L1d 缓存发生在 存储指令从 ROB 退出后的某个时间,即当已知存储是 non-speculative 时,关联的 store-buffer 条目“毕业”并有资格提交缓存并变得全局可见。存储缓冲区将执行与其他内核可以看到的任何内容分离,并将该内核与 cache-miss 存储隔离开来,因此即使在 in-order CPUs.

上也是一个非常有用的功能

在 store-buffer 条目“毕业”之前,当回滚 mis-speculation.[=137 时,它可以与指向它的 ROB 条目一起丢弃=]

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

存储缓冲区实际上是一个循环缓冲区:条目由 front-end(在 alloc/rename 管道阶段)分配,并在存储提交到 L1d 缓存时释放。 (通过 MESI 与其他内核保持一致)。

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

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


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

MMIO 区域(其中读取 do 具有 side-effects,例如让网卡或 SATA 控制器做某事)需要标记为不可缓存,因此 CPU 知道不允许从该物理地址进行推测性读取。 If you get this wrong, your system will be unstable - 我的回答涵盖了很多与您询问的投机负载相同的细节。

高性能 CPUs 有一个包含多个条目的加载缓冲区来跟踪 in-flight 加载,包括在 L1d 缓存中丢失的那些。 (允许 hit-under-miss 和 miss-under-miss 即使在 in-order CPU 秒,仅停止 if/when 一条指令试图读取 load-result 未准备好的寄存器还没有)。

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

另请参阅 以了解有关英特尔 CPU 如何专门处理正在等待的 uops 的更多信息,方法是在数据可能从 L2 到达以进行 L2 命中时积极尝试在周期中启动它们。


脚注 1:这个缺点,结合检测/读取 micro-architectural 状态(缓存行热或冷)到架构状态的时间 side-channel (寄存器值)是启用 Spectre 的原因。 (https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)#Mechanism)

了解 Meltdown 对于理解英特尔 CPUs 选择如何处理 fault-suppression 的细节非常有用. [=22=


And, for sure, read/write operations are supported

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

如果你的意思是atomic RMW,那么这真的不是推测。缓存是全局可见的(共享请求可以随时出现)并且没有办法将其回滚(嗯,除了 whatever Intel does for transactional memory...). You must not ever put a wrong value in cache. See 更多关于如何处理原子 RMW 的信息,特别是在现代 x86 上,通过延迟响应共享 /使负载和 store-commit.

之间的那条线的请求无效

然而,这并不意味着 lock add [rdi], eax 序列化整个管道:Are loads and stores the only instructions that gets reordered? 表明 other 独立指令的推测性 OoO exec 可能发生在周围原子 RMW。 (相对于像 lfence 这样耗尽 ROB 的执行屏障会发生什么)。

许多 RISC ISA 仅通过 load-linked / store-conditional 指令提供原子 RMW,而不是单个原子 RMW 指令。

[read/write ops ...], to some extent at least, due to the fact that the registers themselves, on some CPUs, are physically located on the CPU cache as I understand.

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

寄存器文件像高速缓存一样由 SRAM 构建,但它们是独立的。板上有一些带有 SRAM 内存(不是高速缓存)的微控制器,寄存器 memory-mapped 使用 space 的早期字节。 (例如 AVR)。但是 none 似乎与 out-of-order 执行完全相关;缓存内存的缓存行绝对不是用于完全不同的东西的缓存行,比如保存寄存器值。

将晶体管预算用于推测执行的 high-performance CPU 将缓存与寄存器文件结合起来也不太合理;然后他们会争夺 read/write 个端口。一个具有总读写端口的大型缓存比一个微型快速寄存器文件(许多 read/write 端口)和一个具有几个读取端口的小型(如 32kiB)L1d 缓存昂贵得多(面积和功率) 1个写端口。出于同样的原因,我们使用分离的 L1 缓存,并且有 multi-level 个缓存,而不是现代 CPU 中每个内核只有一个大的私有缓存。 Why is the size of L1 cache smaller than that of the L2 cache in most of the processors?


相关阅读/背景:


  • https://en.wikipedia.org/wiki/Memory_disambiguation - CPU 如何处理从存储缓冲区到负载的转发,或者如果存储实际上比此负载更年轻(按程序顺序排列)则不处理。
  • https://blog.stuffedcow.net/2014/01/x86-memory-disambiguation/ - Store-to-Load Forwarding and Memory Disambiguation in x86 Processors. Very detailed test results and technical discussion of store-forwarding, including from narrow loads that overlap with different parts of a store, and near cache-line boundaries. (https://agner.org/optimize/ 在他的 microarch PDF 中有一些 simpler-to-understand 但关于 store-forwarding 何时慢与快的详细信息较少。)
  • https://github.com/travisdowns/uarch-bench/wiki/Memory-Disambiguation-on-Skylake - 现代 CPUs 动态预测当存在未知地址的较早存储正在运行时加载的内存依赖性。 (即 store-address uop 尚未执行。)如果预测错误,这可能导致必须回滚。
  • - 从 部分 与最近的存储重叠并且部分不给我们提供一些说明如何 CPU 的工作,以及如何 does/doesn 考虑内存(排序)模型是没有意义的。请注意,C++ std::atomic 无法创建执行此操作的代码,尽管 C++20 std::atomic_ref 可以让您执行与对齐的 8 字节原子加载重叠的对齐 4 字节原子存储。