投机性乱序(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缓存小?
相关阅读/背景: