全网最详细的Intel CPU体系结构分析(内核源码) 不一定能看懂 但可以了解一下

2021-11-24|

前段meldown漏洞事件的影响,那段时间也正好在读Paul的论文关于内存屏障的知识,其中有诸多细节想不通,便陷入无尽的煎熬和冥想中,看了《计算机系统结构》、《深入理解计算机系统》、《大话处理器》等经典书籍,也在google上搜了一大堆资料,前前后后、断断续续地折腾了一个多月,终于想通了,现在把自己的思想心得记录下来,希望对有这方面困惑的朋友有些帮助。

本文主要关注以下几个问题。

  • 什么是CPU的流水线?为什么需要流水线?
  • 为什么需要内存屏障?在只有单个Core的CPU中是否还需要内存屏障?
  • 什么是乱序执行?分为几种?
  • MOB和ROB是干什么的?
  • load buffer和store buffer的功能是什么?
  • x86和arm、power中的memory model有什么区别?
  • MESI主要是做什么的?
  • meldown漏洞的原理是什么?

一、CPU指令的执行过程

几乎所有的冯·诺伊曼型计算机的CPU,其工作都可以分为 5 个阶段:取指令、指令译码、执行指令、访存取数、结果写回。

1.取指令阶段

取指令(Instruction Fetch,IF)阶段是将一条指令从主存中取到指令寄存器的过程。 程序计数器 PC 中的数值,用来指示当前指令在主存中的位置。当一条指令被取出后,PC 中的数值将根据指令字长度而自动递增:若为单字长指令,则(PC)+1->PC;若为双字长指令,则(PC)+2->PC,依此类推。

2.指令译码阶段

取出指令后,计算机立即进入指令译码(Instruction Decode,ID)阶段。 在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。在组合逻辑控制的计算机中,指令译码器对不同的指令操作码产生不同的控制电位,以形成不同的微操作序列;在微程序控制的计算机中,指令译码器用指令操作码来找到执行该指令的微程序的入口,并从此入口开始执行。 在传统的设计里,CPU中负责指令译码的部分是无法改变的。不过,在众多运用微程序控制技术的新型 CPU 中,微程序有时是可重写的。

3.执行指令阶段

在取指令和指令译码阶段之后,接着进入执行指令(Execute,EX)阶段。 此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。为此,CPU 的不同部分被连接起来,以执行所需的操作。 例如,如果要求完成一个加法运算,算术逻辑单元 ALU 将被连接到一组输入和一组输出,输入端提供需要相加的数值,输出端将含有最后的运算结果。

4.访存取数阶段

根据指令需要,有可能要访问主存,读取操作数,这样就进入了访存取数(Memory,MEM)阶段。 此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。

5.结果写回阶段

作为最后一个阶段,结果写回(Writeback,WB)阶段把执行指令阶段的运行结果数据“写回”到某种存储形式:结果数据经常被写到CPU内部寄存器中,以便被后续的指令快速地存取;在有些情况下, 结果数据也可被写入相对较慢、但较廉价且容量较大的主存。许多指令还会改变程序状态字寄存器中标志位 的状态,这些标志位标识着不同的操作结果,可被用来影响程序的动作。

在指令执行完毕、结果数据写回之后,若无意外事件(如结果溢出等)发生,计算机就接着从程序计数器PC中取得下一条指令地址,开始新一轮的循环,下一个指令周期将顺序取出下一条指令。

许多新型 CPU 可以同时取出、译码和执行多条指令,体现并行处理的特性。

二、CPU指令流水线

在任一条指令的执行过程中,各个功能部件都会随着指令执行的进程而呈现出时忙时闲的现象。要加快计算机的工作速度,就应使各个功能部件并行工作,即以各自可能的高速度同时、不停地工作,使得各部件的操作在时间上重叠进行,实现流水式作业。 从原理上说,计算机的流水线(Pipeline)工作方式就是将一个计算任务细分成若干个子任务每个子任务都由专门的功能部件进行处理,一个计算任务的各个子任务由流水线上各个功能部件轮流进行处理 (即各子任务在流水线的各个功能阶段并发执行),最终完成工作。这样,不必等到上一个计算任务完成, 就可以开始下一个计算任务的执行。 流水线的硬件基本结构如图2所示。流水线由一系列串联的功能部件(Si)组成,各个功能部件之间设有高速缓冲寄存器(L),以暂时保存上一功能部件对子任务处理的结果,同时又能够接受新的处理任务。在一个统一的时钟(C)控制下,计算任务从功能部件的一个功能段流向下一个功能段。在流水线中, 所有功能段同时对不同的数据进行不同的处理,各个处理步骤并行地操作。

当任务连续不断地输入流水线时,在流水线的输出端便连续不断地输出执行结果,流水线达到不间断流水的稳定状态,从而实现了子任务级的并行

当指令流不能顺序执行时,流水过程会中断(即断流)。为了保证流水过程的工作效率,流水过程不应经常断流。在一个流水过程中,实现各个流水过程的各个功能段所需要的时间应该尽可能保持相等,以避免产生瓶颈,导致流水线断流。

流水线技术本质上是将一个重复的时序过程分解成若干个子过程,而每一个子过程都可有效地在其专用功能段上与其他子过程同时执行。采用流水线技术通过硬件实现并行操作后,就某一条指令而言,其执行速度并没有加快,但就程序执行过程的整体而言,程序执行速度大大加快。

流水线技术适合于大量的重复性的处理。

前面我提到过CPU 中一个指令周期的任务分解。假设指令周期包含取指令(IF)、指令译码(ID)、 指令执行(EX)、访存取数(MEM)、结果写回(WB)5 个子过程(过程段),流水线由这 5个串联的过程段 组成,各个过程段之间设有高速缓冲寄存器,以暂时保存上一个过程段的任务处理的结果,在统一的时钟信号控制下,数据从一个过程段流向相邻的过程段。

非流水计算机的时空图如下:

标量流水计算机工作方式

标量(Scalar)流水计算机是只有一条指令流水线的计算机。图4表示标量流水计算机的时空图。

当流水线满载时,每一个时钟周期可以执行 2 条以上的指令。因此,图5中仅用了 9 个时钟周期就完成了10条指令,每条指令平均用时 0.9 个时钟周期。 超标量流水计算机是时间并行技术和空间并行技术的综合应用。

三、指令的相关性

指令流水线的一个特点是流水线中的各条指令之间存在一些相关性,使得指令的执行受到影响。要使流水线发挥高效率,就要使流水线连续不断地流动,尽量不出现断流的情况。然而,由于流水过程中存在的相关性冲突,断流现象是不可避免的

1.数据相关

在流水计算机中,指令的处理是重叠进行的,前一条指令还没有结束,第二、三条指令就陆续开始工 作。由于多条指令的重叠处理,当后继指令所需的操作数刚好是前一条指令的运算结果时,便发生数据相关冲突。由于这两条指令的执行顺序直接影响到操作系统读取的内容,必须等前一条指令执行完毕后才能执行后一条指令。在这种情况下,这两条指令就是与数据相关的。因此,数据相关是由于指令之间存在数据依赖性而引起的。根据指令间对同一寄存器读和写操作的先后次序关系,可将数据相关性分为写后读(Read-AfterWrite,RAW)相关、读后写(Write-After-Read,WAR)相关、写后写(Write-After-Write,WAW)相关三种类型。

解决数据相关冲突的办法如下:

⑴采用编译的方法 编译程序通过在两条相关指令之间插入其他不相关的指令(或空操作指令)而推迟指令的执行,使数据相关消失,从而产生没有相关性的程序代码。这种方式简单,但降低了运行效率。

⑵由硬件监测相关性的存在,采用数据旁路技术设法解决数据相关 当前一条指令要写入寄存器而下一条指令要读取同一个寄存器时,在前一条指令执行完毕、结果数据还未写入寄存器前,由内部数据通路把该结果数据直接传递给下一条指令,也就是说,下一条指令所需的 操作数不再通过读取寄存器获得,而是直接获取。这种方式效率较高,但控制也较为复杂。

2.资源相关

所谓资源相关,是指多条指令进入流水线后在同一机器周期内争用同一个功能部件所发生的冲突。 例如,在图 4所示的标量流水计算机中,在第 4 个时钟周期时,第 1 条指令处于访存取数(MEM) 阶段,而第 4 条指令处于取指令(IF)阶段。如果数据和指令存放在同一存储器中且存储器只有一个端口,这样便会发生这两条指令争用存储器的资源相关冲突。 因为每一条指令都可能需要 2 次访问存储器(读指令和读写数据),在指令流水过程中,可能会有 2 条件同时需要访问存储器,导致资源相关冲突解决资源相关冲突的一般办法是增加资源,例如增设一个存储器,将指令和数据分别放在两个存储器中。

3.控制相关

控制相关冲突是由转移指令引起的。当执行转移指令时,依据转移条件的产生结果,可能顺序取下一 条指令,也可能转移到新的目标地址取指令。若转移到新的目标地址取指令,则指令流水线将被排空,并等待转移指令形成下一条指令的地址,以便读取新的指令,这就使得流水线发生断流。 为了减小转移指令对流水线性能的影响,通常采用以下两种转移处理技术:

⑴延迟转移法 编译程序重排指令序列来实现。其基本思想是“先执行再转移”,即发生转移时并不排空指令流水线,而是继续完成下几条指令。如果这些后继指令是与该转移指令结果无关的有用指令,那么延迟损失时间片正好得到了有效的利用。

⑵转移预测法 用硬件的方法来实现。依据指令过去的行为来预测将来的行为,即选择出现概率较高的分支进行预取。通过使用转移取和顺序取两路指令选取队列以及目标指令 Cache,可将转移预测提前到取指令阶段进行,以获得良好的效果。

四、指令的动态执行技术

1.指令调度

为了减少指令相关性对执行速度的影响,可以在保证程序正确性的前提下,调整指令的顺序,即进行指令调度。 指令调度可以由编译程序进行也可以由硬件在执行的时候进行,分别称为静态指令调度动态指令调度静态指令调度是指编译程序通过调整指令的顺序来减少流水线的停顿,提高程序的执行速度;动态 指令调度用硬件方法调度指令的执行以减少流水线停顿

流水线中一直采用的有序(in-order)指令启动是限制流水线性能的主要因素之一。如果有一条指令在流水线中停顿了,则其后的指令就都不能向前流动了,这样,如果相邻的两条指令存在相关性,流水线就将发生停顿,如果有多个功能部件,这些部件就有可能被闲置。消除这种限制流水线性能的因素从而提高指令执行速度,其基本思想就是允许指令的执行是无序的(out-of-order,也称乱序),也就是说,在保持指令间、数据间的依赖关系的前提下,允许不相关的指令的执行顺序与程序的原有顺序有所不同,这一思想是实行动态指令调度的前提。

2.乱序执行技术

乱序执行(Out-of-order Execution)是以乱序方式执行指令,即 CPU 允许将多条指令不按程序规定的顺序而分开发送给各相应电路单元进行处理。这样,根据各个电路单元的状态和各指令能否提前执行的具体情况分析,将能够提前执行的指令立即发送给相应电路单元予以执行,在这期间不按规定顺序执行指令;然后由重新排列单元将各执行单元的结果按指令顺序重新排列。乱序执行的目的,就是为了使 CPU 内部电路满负荷运转,并相应提高 CPU 运行程序的速度

实现乱序执行的关键在于取消传统的“取指”和“执行”两个阶段之间指令需要线性排列的限制,而使用一个指令缓冲池来开辟一个较长的指令窗口,允许执行单元在一个较大的范围内调遣和执行已译码的程序指令流

3.分支预测

分支预测(Branch Prediction)是对程序的流程进行预测,然后读取其中一个分支的指令。采用分支预测的主要目的是为了提高 CPU的运算速度。 分支预测的方法有静态预测和动态预测两类:静态预测方法比较简单,如预测永远不转移、预测永远转移、预测后向转移等等,它并不根据执行时的条件和历史信息来进行预测,因此预测的准确性不可能很高;动态预测方法则根据同一条转移指令过去的转移情况来预测未来的转移情况。 由于程序中的条件分支是根据程序指令在流水线处理后的结果来执行的,所以当 CPU 等待指令结果时, 流水线的前级电路也处于等待分支指令的空闲状态,这样必然出现时钟周期的浪费。如果 CPU 能在前条指令结果出来之前就预测到分支是否转移,那么就可以提前执行相应的指令,这样就避免了流水线的空闲等待,也就相应提高了 CPU 的运算速度。但另一方面,一旦前条指令结果出来后证明分支预测是错误的,那么就必须将已经装入流水线执行的指令和结果全部清除,然后再装入正确的指令重新处理,这样就比不进行分支预测而是等待结果再执行新指令还要慢了

因此,分支预测的错误并不会导致结果的错误,而只是导致流水线的停顿,如果能够保持较高的预测 准确率,分支预测就能提高流水线的性能。

五、实例分析

前面的知识只是一个理论基础铺垫,下面我们就结合一款真实的CPU架构进行对应分析,图6和图7分别是x86和ARM体系结构的内核架构图(都是具有OoOE特性的CPU架构),可以看到他们基本的组成都是一样的(虽然x86是CISC而ARM是RISC,但是现代x86内部也是先把CISC翻译成RISC的),因此我在这里就只分析x86结构。

1.取指令阶段(IF)

处理器在执行指令之前,必须先装载指令。指令会先保存在 L1 缓存的 I-cache (Instruction-cache)指令缓存当中,Nehalem指令拾取单元使用 128bit 带宽的通道从 I-cache 中读取指令。这个 I-cache 的大小为 32KB,采用了 4 路组相连,在后面的存取单元介绍中我们可以得知这种比 Core 更少的集合关联数量是为了降低延迟。

为了适应超线程技术,RIP(Relative Instruction Point,相对指令指针)的数量也从一个增加到了两个,每个线程单独使用一个。

指令拾取单元包含了分支预测器(Branch Predictor),分支预测是在 Pentium Pro 处理器开始加入的功能,预测如 if then 这样的语句的将来走向提前读取相关的指令并执行的技术,可以明显地提升性能。指令拾取单元也包含了 Hardware Prefetcher根据历史操作预先加载以后会用到的指令来提高性能,这会在后面得到详细的介绍。

当分支预测器决定了走向一个分支之后,它使用 BTB(Branch Target Buffer,分支目标缓冲区)来保存预测指令的地址。Nehalem 从以前的一级 BTB 升级到了两个级别,这是为了适应很大体积的程序(数据库以及 ERP 等应用,跳转分支将会跨过很大的区域并具有很多的分支)。Intel 并没有提及 BTB 详细的结构。与BTB 相对的 RSB(Return Stack Buffer,返回堆栈缓冲区)也得到了提升,RSB 用来保存一个函数或功能调用结束之后的返回地址,通过重命名的 RSB 来避免多次推测路径导致的入口/出口破坏。RSB 每个线程都有一个,一个核心就拥有两个,以适应超线程技术的存在

指令拾取单元使用预测指令的地址来拾取指令,它通过访问 L1 ITLB 里的索引来继续访问 L1 ICache,128 条目的小页面 L1 ITLB 按照两个线程静态分区,每个线程可以获得 64 个条目,这个数目比 Core 2 地少。当关闭超线程时,单独的线程将可以获得全部的 TLB 资 源。除了小页面 TLB 之外,Nehalem 还每个线程拥有 7 个条目的全关联(Full Associativity) 大页面 ITLB,这些 TLB 用于访问 2M/4M 的大容量页面,每个线程独立,因此关闭超线程不会让你得到 14 个大页面 ITLB 条目。

指令拾取单元通过 128bit 的总线将指令从 L1 ICache 拾取到一个 16Bytes(刚好就是 128bit)的预解码拾取缓冲区。128 位的带宽让人有些迷惑不解,Opteron 一早就已经使用 了 256bit 的指令拾取带宽。最重要的是,L1D 和 L1I 都是通过 256bit 的带宽连接到 L2 Cache 的。

由于一般的CISC x86指令都小于4Bytes(32位x86指令;x86指令的特点就是不等长), 因此一次可以拾取 4 条以上的指令,而预解码拾取缓冲区的输出带宽是 6 指令每时钟周期, 因此可以看出指令拾取带宽确实有些不协调,特别是考虑到 64 位应用下指令会长一些的情 况下(解码器的输入输出能力是 4 指令每时钟周期,因此 32 位下问题不大)。

指令拾取结束后会送到 18 个条目的指令队列,在 Core 架构,送到的是 LSD 循环流缓冲区,在后面可以看到,Nehalem 通过将 LSD 移动后更靠后的位置来提高性能。

2.指令译码阶段(ID)

再将指令充填到可容纳 18 条目的指令队列之后,就可以进行解码工作了。解码是类 RISC (精简指令集或简单指令集)处理器导致的一项设计,从 Pentium Pro 开始在 IA 架构出现。 处理器接受的是 x86 指令(CISC 指令,复杂指令集),而在执行引擎内部执行的却不是x86 指令,而是一条一条的类 RISC 指令,Intel 称之为 Micro Operation——micro-op,或者写 为 μ-op,一般用比较方便的写法来替代掉希腊字母:u-op 或者 uop。相对地,一条一条地 x86 指令就称之为 Macro Operation或 macro-op。

RISC 架构的特点就是指令长度相等,执行时间恒定(通常为一个时钟周期),因此处理器设计起来就很简单,可以通过深长的流水线达到很高的频率,IBM 的 Power6 就可以轻松地达到 4.7GHz 的起步频率。和 RISC 相反,CISC 指令的长度不固定,执行时间也不固定,因此 Intel 的 RISC/CISC 混合处理器架构就要通过解码器 将 x86 指令翻译为 uop,从而获得 RISC 架构的长处,提升内部执行效率

和 Core 一样,Nehalem 的解码器也是 4 个(3 个简单解码器加 1 个复杂解码器)。简单解码器可以将一条 x86 指令(包括大部分 SSE 指令在内)翻译为一条 uop,而复杂解码器则将一些特别的(单条)x86 指令翻译为 1~4 条 uops——在极少数的情况下,某些指令需要通过 额外的可编程 microcode 解码器解码为更多的 uops(有些时候甚至可以达到几百个,因为 一些 IA 指令很复杂,并且可以带有很多的前缀/修改量,当然这种情况很少见),下图 Complex Decoder 左方的 ucode 方块就是这个解码器,这个解码器可以通过一些途径进行升级或者扩展,实际上就是通过主板 Firmware 里面的 Microcode ROM 部分。

之所以具有两种解码器,是因为仍然是关于 RISC/CISC 的一个事实: 大部分情况下(90%) 的时间内处理器都在运行少数的指令,其余的时间则运行各式各样的复杂指令(不幸的是, 复杂就意味着较长的运行时间),RISC 就是将这些复杂的指令剔除掉,只留下最经常运行的指令(所谓的精简指令集),然而被剔除掉的那些指令虽然实现起来比较麻烦,却在某些领域确实有其价值,RISC 的做法就是将这些麻烦都交给软件,CISC 的做法则是像现在这样: 由硬件设计完成。因此 RISC 指令集对编译器要求很高,而 CISC 则很简单。对编程人员的要求也类似。

3、循环流检测

在解码为 uop 之后 Nehalem 会将它们都存放在一个叫做 uop LSD Buffer 的缓存区。在Core 2 上,这个 LSD Buffer 是出现在解码器前方的,Nehalem 将其移动到解码器后方,并相对加大了缓冲区的条目。Core 2 的 LSD 缓存区可以保存 18 个 x86 指令而 Nehalem 可以保 存 28 个 uop,从前文可以知道,大部分 x86 指令都可以解码为一个 uop,少部分可以解码 为 1~4 个 uop,因此 Nehalem 的 LSD 缓冲区基本上可以相当于保存 21~23 条x86 指令,比 Core 2 要大上一些。

LSD 循环流监测器也包含在解码部分,它的作用是: 假如程序使用的循环段(如 for..do/do..while 等)少于 28 个 uops,那么 Nehalem 就可以将这个循环保存起来,不再需要重新通过取指单元、分支预测操作,以及解码器,Core 2 的 LSD 放在解码器前方,因此无法省下解码器的工作。

Nehalem LSD 的工作比较像 NetBurst 架构的 Trace Cache,其也是保存 uops,作用也是部分地去掉一些严重的循环,不过由于 Trace Cache 与此同时担当着类似于 Core/Nehalem 架构的 Reorder Buffer 乱序缓冲区的作用,容量比较大(可以保存 12k uops,准确的大小 是 20KB),因此在 cache miss 的时候后果严重(特别是在 SMT 同步多线程之后,miss 增加 倍的情况下),LSD 的小数目设计显然会好得多。不过笔者认为 28 个 uop 条目有些少,特 别是考虑到 SMT 技术带来的两条线程都同时使用这个 LSD 的时候。

在 LSD 之后,Nehalem 将会进行 Micro-ops Fusion,这也是前端(The Front-End)的最后一个功能,在这些工作都做完之后,uops 就可以准备进入执行引擎了。

4.乱序执行指令阶段(OoOE)

OoOE— Out-of-Order Execution 乱序执行也是在 Pentium Pro 开始引入的,它有些类似于多线程的概念。乱序执行是为了直接提升 ILP(Instruction Level Parallelism)指令级并行化的设计,在多个执行单元的超标量设计当中,一系列的执行单元可以同时运行一些没有数据关联性的若干指令只有需要等待其他指令运算结果的数据会按照顺序执行,从而总体提升了运行效率。乱序执行引擎是一个很重要的部分,需要进行复杂的调度管理。

首先,在乱序执行架构中,不同的指令可能都会需要用到相同的通用寄存器(GPR,General Purpose Registers),特别是在指令需要改写该通用寄存器的情况下——为了让这些指令们能并行工作,处理器需要准备解决方法。一般的 RISC 架构准备了大量的GPR, 而x86 架构天生就缺乏 GPR(x86具有8个GPR,x86-64 具有 16 个,一般 RISC 具有 32 个,IA64 则具有 128 个),为此 Intel 开始引入重命名寄存器(Rename Register),不同的指令可以通过具有名字相同但实际不同的寄存器来解决。

此外,为了 SMT 同步多线程,这些寄存器还要准备双份,每个线程具有独立的一份。

乱序执行从Allocator定位器开始,Allocator 管理着RAT(Register Alias Table,寄存器别名表)ROB(Re-Order Buffer,重排序缓冲区)RRF(Retirement Register File,退回寄存器文件)。在 Allocator 之前,流水线都是顺序执行的,在 Allocator 之后,就可以进入乱序执行阶段了。在每一个线程方面,Nehalem 和 Core 2 架构相似,RAT 将重命名的、虚拟的寄存器(称为 Architectural Register 或 Logical Register)指向ROB 或者RRFRAT 是一式两份,每个线程独立每个 RAT 包含了 128 各重命名寄存器RAT 指向在 ROB 里面的最近的执行寄存器状态,或者指向RRF保存的最终的提交状态。

ROB(Re-Order Buffer,重排序缓冲区)是一个非常重要的部件,它是将乱序执行完毕的指令们按照程序编程的原始顺序重新排序的一个队列,以保证所有的指令都能够逻辑上实现正确的因果关系打乱了次序的指令们(分支预测、硬件预取)依次插入这个队列,当一条指令通过 RAT 发往下一个阶段确实执行的时候这条指令(包括寄存器状态在内)将被加入 ROB 队列的一端,执行完毕的指令(包括寄存器状态)将从 ROB 队列的另一端移除(期间这些指令的数据可以被一些中间计算结果刷新),因为调度器是 In-Order 顺序的,这个队列(ROB)也就是顺序的从 ROB 中移出一条指令就意味着指令执行完毕了,这个阶段叫做 Retire 回退,相应地 ROB 往往也叫做 Retirement Unit(回退单元),并将其画为流水线的最后一部分

在一些超标量设计中,Retire 阶段会将 ROB 的数据写入 L1D 缓存(这是将MOB集成到ROB的情况),而在另一些设计里, 写入 L1D 缓存由另外的队列完成。例如,Core/Nehalem 的这个操作就由 MOB(Memory Order Buffer,内存重排序缓冲区)来完成。

ROB 是乱序执行引擎架构中都存在的一个缓冲区,重新排序指令的目的是将指令们的寄存器状态依次提交到RRF退回寄存器文件当中以确保具有因果关系的指令们在乱序执行中可以得到正确的数据从执行单元返回的数据会将先前由调度器加入ROB 的指令刷新数据部分标志为结束(Finished),再经过其他检查通过后才能标志为完毕(Complete)一旦标志为完毕,它就可以提交数据并删除重命名项目并退出ROB 。提交状态的工作由 Retirement Unit(回退单元)完成,它将确实完毕的指令包含的数据写入RRF(“确实” 的意思是,非猜测执性、具备正确因果关系,程序可以见到的最终的寄存器状态)。和 RAT 一样,RRF 也同时具有两个,每个线程独立Core/Nehalem 的 Retirement Unit 回退单元每时钟周期可以执行 4 个 uops 寄存器文件写入,和 RAT 每时钟 4 个 uops 的重命名一致。

由于 ROB 里面保存的指令数目是如此之大(128 条目),因此一些人认为它的作用是用来从中挑选出不相关的指令来进入执行单元,这多少是受到一些文档中的 Out-of-Order Window 乱序窗口这个词的影响(后面会看到ROB 会和 MOB 一起被计入乱序窗口资源中)。

ROB 确实具有 RS 的一部分相似的作用,不过,ROB 里面的指令是调度器(dispacher)通过 RAT发往 RS 的同时发往ROB的(里面包含着正常顺序的指令和猜测执行的指令,但是乱序执行并不是从ROB中乱序挑选的),也就是说,在“乱序”之前,ROB 的指令就已经确定了指令并不是在 ROB 当中乱序挑选的(这是在RS当中进行),ROB 担当的是流水线的最终阶段: 一个指令的 Retire回退单元;以及担当中间计算结果的缓冲区。
RS(Reservation Station,中继站): 等待源数据到来以进行OoOE乱序执行(没有数据的指令将在 RS 等待), ROB(ReOrder Buffer,重排序缓冲区): 等待结果到达以进行 Retire 指令回退 (没有结果的指令将在 ROB等待)。

Nehalem 的 128 条目的 ROB 担当中间计算结果的缓冲区它保存着猜测执行的指令及其数据猜测执行允许预先执行方向未定的分支指令。在大部分情况下,猜测执行工作良好——分支猜对了,因此其在 ROB 里产生的结果被标志为已结束,可以立即地被后继指令使用而不需要进行 L1 Data Cache 的 Load 操作(这也是 ROB 还有另一个重要用处,典型的 x86 应用中 Load 操作是如此频繁,达到了几乎占 1/3 的地步,因此 ROB 可以避免大量的Cache Load 操作,作用巨大)。在剩下的不幸的情况下,分支未能按照如期的情况进行,这时猜测的分支指令段将被清除,相应指令们的流水线阶段清空,对应的寄存器状态也就全都无效了,这种无效的寄存器状态不会也不能出现在 RRF 里面

重命名技术并不是没有代价的,在获得前面所说的众多的优点之后,它令指令在发射的时候需要扫描额外的地方来寻找到正确的寄存器状态,不过总体来说这种代价是非常值得的。RAT可以在每一个时钟周期重命名 4 个 uops 的寄存器,经过重命名的指令在读取到正确的操作数并发射到统一的RS(Reservation Station,中继站,Intel 文档翻译为保留站点) 上。RS 中继站保存了所有等待执行的指令。

和 Core 2 相比,Nehalem 的 ROB 大小和 RS 大小都得到了提升,ROB 重排序缓冲区从 96 条目提升到 128 条目(鼻祖 Pentium Pro 具有 40 条),RS 中继站从 32 提升到 36(Pentium Pro 为 20),它们都在两个线程(超线程中的线程)内共享,不过采用了不同的策略:ROB 是采用了静态的分区方法,而 RS 则采用了动态共享,因为有时候会有一条线程内的指令因 等待数据停滞,这时另一个线程就可以获得更多的 RS 资源。停滞的指令不会发现 RS,但是仍然会占用 ROB 条目。由于 ROB 是静态分区,因此在开启 HTT 在这情况下,每一个线程只能 分到 64 条,不算多,在一些极少数的应用上,我们应该可以观察到一些应用开启 HTT 后悔 速度降低,尽管可能非常微小。

5、执行单元

在为 SMT 做好准备工作并打乱指令的执行顺序之后(指的是分支预测、硬件预取),uops 通过每时钟周期 4 条的速度进入 Reservation Station 中继站(保留站),总共 36 条目的中继站 uops 就开始等待超标量(Superscaler)执行引擎乱序执行了。自从 Pentium 开始,Intel 就开始在处理器里面采用了超标量设计(Pentium 是两路超标量处理器),超标量的意思就是多个执行单元,它可以同时执行多条没有相互依赖性的指令,从而达到提升 ILP 指令级别并行化的目的。Nehalem 具备 6 个执行端口,每个执行端口具有多个不同的单元以执行不同的任务,然而同一时间只能有一条指令(uop)进入执行端口,因此也可以认为 Nehalem 有 6 个“执行单元”,在每个时钟周期内可以执行最多 6 个操作(或者说,6 条指令),和 Core 一样。

36 条目的中继站指令在分发器的管理下,挑选出尽量多的可以同时执行的指令(也就是乱序执行的意思)——最多 6 条——发送到执行端口。 这些执行端口并不都是用于计算,实际上,有三个执行端口是专门用来执行内存相关的操作的,只有剩下的三个是计算端口,因此,在这一点上 Nehalem 实际上是跟 Core 架构一 样的,这也可以解释为什么有些情况下,Nehalem 和 Core 相比没有什么性能提升。

计算操作分为两种: 使用 ALU(Arithmetic Logic Unit,算术逻辑单元)的整数(Integer) 运算和使用 FPU(Floating Point Unit,浮点运算单元)的浮点(Floating Point)运算。SSE 指令(包括 SSE1 到 SSE4)是一种特例,它虽然有整数也有浮点,然而它们使用的都是 128bit 浮点寄存器,使用的也大部分是 FPU 电路。在 Nehalem 中,三个计算机端口都可以做整数运算(包括 MMX)或者SSE 运算(浮点运算不太一样,只有两个端口可以进行浮点 ADD 和 MUL/DIV 运算,因此每个时钟周期最多进行 2 个浮点计算,这也是目前 Intel 处理器浮点性能不如整数性能突出的原因),不过每一个执行端口都不是完全一致:只有端口 0 有浮点乘和除功能,只有端口 5 有分支能力(这个执行单元将会与分支预测单元连接),其他 FP/SSE 能力也不尽相同,这些不对称之处都由统一的分发器来理解,并进行指令的调度管理。没有采用完全对称的设计可能是基于统计学上的考虑。和 Core 一样,Nehalem 的也没有采用 Pentium 4 那样的 2 倍频的 ALU 设计(在 Pentium 4,ALU 的运算频率是 CPU 主频的两倍, 因此整数性能明显要比浮点性能突出)。

不幸的是,虽然可以同时执行的指令很多,然而在流水线架构当中运行速度并不是最 “宽”的单元来决定的,而是由最“窄”的单元来决定的。这就是木桶原理,Opteron的解码器后端只能每时钟周期输出 3 条 uops,而 Nehalem/Core2 则能输出 4 条,因此它们的实际最大每时钟运行指令数是 3/4,而不是 6。同样地,多少路超标量在这些乱序架构处理器中也不再按照运算单元来划分,Core Duo 及之前(到 Pentium Pro 为止)均为三路超标量处理器,Core 2/Nehalem 则为四路超标量处理器。可见在微架构上,Nehalem/Core 显然是 要比其他处理器快一些。顺便说一下,这也是 Intel 在超线程示意图中,使用 4 个宽度的方 块来表示而不是 6 个方块的原因。

6、存取单元

运算需要用到数据,也会生成数据,这些数据存取操作就是存取单元所做的事情,实际 上,Nehalem 和 Core 的存取单元没什么变化,仍然是 3 个。

三个存取单元中,


标签: 指令 执行 可以 寄存器 数据 流水线 一个 store 结果 操作
出处: https://www.toutiao.com/a7033315848764948999/

推荐

文明发言,请先登录

文明上网理性发言,请遵守国家法律法规。

最新评论

©2003- 黑基网 黑名单存档手机版网站地图免责条款法律声明隐私保护