一个具有执行推测的CPU分支是否可能包含访问RAM的操作码?

8
据我所知,当CPU在执行某个代码片段之前进行预测时,它会“备份”寄存器状态,以便如果预测结果错误(使分支无用)--寄存器状态将被安全地恢复,而不会破坏“状态”。
那么,我的问题是:一个具有推测性的CPU分支能否包含访问RAM的操作码? 我的意思是,访问RAM不是一个“原子”操作 - 一个简单的操作码从内存中读取数据可能会导致实际的RAM访问,如果数据当前不在CPU缓存中,这可能会从CPU的角度来看变得极其耗时。
而且,如果确实允许在推测分支中进行这样的访问,那么只能进行读操作吗?因为我只能假设,如果需要回滚一个分支并执行“回滚”操作,那么根据其大小,撤销写操作可能会变得极其缓慢和棘手。 并且,肯定支持读/写操作,至少在一定程度上是由于一些CPU上的寄存器本身就是物理上位于CPU缓存中的。
因此,也许更精确的表述应该是:具有推测性的代码的限制是什么?
1个回答

21
投机性乱序(OoO)执行的基本规则如下:
1. 保持指令按程序顺序顺序运行的错觉。 2. 确保推测仅限于可以在发现错误推测时回滚的内容,并且其他核心无法观察到错误值。物理寄存器、跟踪指令顺序的后端本身是可以的,但缓存不行。缓存与其他核心一致,因此存储必须在非推测之后才能提交到缓存。
通常通过将所有内容视为推测状态直到退休来实现乱序执行。每个加载或存储都可能出错,每个浮点指令都可能引发浮点异常。分支与异常相比只是特殊之处在于分支预测错误并不罕见,因此处理早期检测和回滚分支预测错误的特殊机制会很有帮助。
是的,可缓存的负载可以进行推测性执行和乱序执行,因为它们没有副作用。
由于存在存储缓冲区,存储指令也可以进行推测性执行。实际执行存储操作只是将地址和数据写入存储缓冲区。(相关:英特尔硬件上存储缓冲区的大小是多少?存储缓冲区到底是什么? 这个问题更加技术性,更加关注x86。我认为这个答案适用于大多数ISA。)
在存储指令从重排序缓冲区(ROB)中退休后,L1d缓存的提交会在一段时间后发生,即当存储被确认为非推测性时,相关的存储缓冲区条目“毕业”并有资格提交到缓存并在全局范围内可见。存储缓冲区将执行与其他核心可见的内容解耦,并且还使该核心免受缓存未命中存储的影响,因此即使在顺序执行的CPU上,这也是一个非常有用的功能。
在存储缓冲区条目“毕业”之前,当发生错误推测时,它可以与指向它的ROB条目一起被丢弃。
这就是为什么即使在强有序的硬件内存模型中,仍然允许StoreLoad重排序(参见链接1:https://preshing.com/20120930/weak-vs-strong-memory-models/)- 这几乎是为了良好的性能而不使后续加载等待早期存储实际提交。
存储缓冲区实际上是一个循环缓冲区:由前端分配(在分配/重命名流水线阶段)并在将存储提交到L1d缓存时释放。(通过MESI与其他核心保持一致)。
像x86这样的强有序内存模型可以通过按顺序从存储缓冲区到L1d进行提交来实现。条目按程序顺序分配,因此存储缓冲区基本上可以是硬件中的循环缓冲区。如果存储缓冲区的头部是尚未准备好的缓存行,则弱有序ISA可以查看较新的条目。
一些ISA(尤其是弱排序的)还会合并存储缓冲区条目,从而将一对32位存储合并为一个8字节的L1d提交,例如这个例子

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

MMIO区域(其中读取确实具有副作用,例如使网络卡或SATA控制器执行某些操作)需要标记为不可缓存,以便CPU知道不允许从该物理地址进行推测性读取。如果你弄错了,你的系统将变得不稳定 - 我在那里的回答涵盖了很多你关于推测加载的同样细节。

高性能CPU具有多个条目的加载缓冲区,用于跟踪正在进行中的加载,包括在L1d缓存中未命中的加载(即使在顺序CPU上也允许在未命中时暂停,并且仅在指令尝试读取尚未准备好的加载结果寄存器时暂停)。

在一个乱序执行的CPU中,当一个加载地址准备好之前,它也允许乱序执行。当数据最终到达时,等待来自加载结果的输入的指令变得可以运行(如果它们的其他输入也准备好了)。因此,加载缓冲区条目必须与调度器(在某些CPU中称为预约站)连接起来。
有关英特尔CPU如何处理等待中的uops并试图在可能从L2到达的周期上启动它们的更多信息,请参阅关于RIDL漏洞和“重播”加载的内容

脚注1: 这个缺点与一种时序侧信道相结合,可以将微体系结构状态(缓存行热或冷)检测/读取转化为架构状态(寄存器值),从而使得 Spectre 成为可能。(https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)#Mechanism)

理解 Meltdown 对于了解英特尔 CPU 如何选择处理那些在错误路径上的推测加载并进行故障抑制非常有用。 http://blog.stuffedcow.net/2018/05/meltdown-microarchitecture/


而且,肯定支持读写操作。
是的,通过将它们解码为逻辑上分离的加载/ALU/存储操作来实现,如果你说的是现代x86,它会解码为指令uops。加载操作与普通加载类似,存储操作将ALU结果放入存储缓冲区。所有这三种操作都可以像单独编写指令一样由乱序处理器后端正常调度。
如果你指的是原子的读改写操作(RMW),那么它不能真正是投机的。缓存是全局可见的(共享请求可以随时到来),并且没有办法回滚它(好吧,除了英特尔在事务内存方面所做的一切...)。你绝不能将错误的值放入缓存中。有关如何处理原子RMW操作的更多信息,特别是在现代x86上,可以延迟对该行的加载和存储提交之间的共享/使无效请求的响应,请参阅num++对'int num'能否是原子操作?

然而,这并不意味着 lock add [rdi], eax 会序列化整个流水线:负载和存储是唯一会重排序的指令吗? 表明,在原子 RMW 周围发生其他独立指令的推测性乱序执行是可能的。(与类似 lfence 的执行障碍引起 ROB 领空的情况相比)。

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

[读/写操作...], 至少在某种程度上,这是因为在某些 CPU 上,寄存器本身实际上是位于 CPU 缓存上的,据我所知。

哎呀?这是错误的前提,那种逻辑没有意义。缓存必须始终保持正确,因为另一个核心随时可能要求共享它。而寄存器只属于该核心私有。

寄存器文件由SRAM构建,类似于缓存,但是它们是独立的。有一些带有SRAM内存(不是缓存)的微控制器,并且寄存器是通过该空间的早期字节进行内存映射的。(例如AVR)。但是所有这些与乱序执行似乎完全不相关;缓存内存的缓存行绝对不是用于完全不同的事物,比如保存寄存器值。

这也不太可能,一个将晶体管预测执行作为花费晶体管预算的高性能CPU会将缓存与寄存器文件结合在一起;然后它们将竞争读/写端口。一个拥有总共读写端口的大型缓存比一个小型(例如32kiB)快速寄存器文件(许多读写端口)和一个带有几个读端口和一个写端口的小型L1d缓存要昂贵得多(面积和功耗)。出于同样的原因,我们在现代CPU中使用分离的L1缓存,并且有多级缓存,而不仅仅是每个核心一个大的私有缓存。 为什么大多数处理器的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中提供了一些更简单易懂但不太详细的关于存储转发何时变慢和快的信息。)
  • https://github.com/travisdowns/uarch-bench/wiki/Memory-Disambiguation-on-Skylake - 现代CPU在有早期存储正在执行但地址未知的情况下,动态预测加载的内存依赖关系。(即尚未执行存储地址uop。)如果预测错误,可能需要回滚。
  • 全局不可见的加载指令 - 从部分重叠最近的存储进行存储转发,部分不重叠,这给我们提供了一个特殊情况,可以揭示CPU的工作原理以及如何思考内存(排序)模型是否合理。请注意,C++ std::atomic无法创建执行此操作的代码,尽管C++20 std::atomic_ref可以让您执行与对齐的8字节原子加载重叠的对齐的4字节原子存储。

2
非常感谢您提供如此丰富详细的答案。 - golosovsky
3
哇,多好的回答! - Margaret Bloom
2
@MargaretBloom:谢谢。我之前写过一些答案,想解释一下存储缓冲区是什么以及它的作用,但最终却陷入了特定细节,并变得非常技术化。这次我想我成功地写了一个更适合初学者的有关相关概念的真正介绍。 - Peter Cordes
2
典型的好答案。缓存可以包含推测状态;硬件事务性内存可以通过允许将推测写入缓存并使其对其他代理不可见来实现。然而,让一个已经复杂的概念变得更加复杂可能不是明智之举。更加离谱的是,理论上可以缓存MMIO访问,尽管保证正确行为的复杂性会限制这种做法的总体回报(许多I/O读取没有副作用,甚至一些写入也是安全的,类似于一些推测堆栈/TLS写入)。缓存的MMIO甚至是“不必要的复杂化”。 - user2467198

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