购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第3章
CPU微架构

本章简要介绍影响性能的关键CPU架构和微架构特性,其目标并非覆盖CPU架构细节和设计时的利弊权衡,这些在文献(Hennessy&Patterson,2011)中有全面的介绍。这里将快速回顾对软件性能有直接影响的CPU硬件特性。

3.1 指令集架构

指令集是软件用来与硬件通信的词汇集合,指令集架构(Instruction Set Architecture,ISA)定义了软件和硬件之间的通信协议。Intel x86、ARM v8、RISC-V是当今广泛使用指令集架构的实例,它们都是64位架构,即所有地址计算都使用64位。ISA开发者通常要确保符合规范的软件或固件能在使用该规范构建的任何处理器上执行。广泛部署的ISA特许经营组织通常还要保证向后兼容性以便为第 X 代版本处理器编写的代码能够继续在第 X+i 代上运行。

大多数现代架构可以归类为基于通用寄存器的加载和存储型架构,在这种架构下,操作数被明确指定,只能使用加载和存储指令访问内存。除提供最基本的功能(例如加载、存储、控制、使用整数及浮点数的标量算术操作)之外,广泛部署的架构还在继续增强其ISA以支持新的计算范式,包括增强的向量处理指令(例如Intel AVX2、AVX512、ARM SVE)和矩阵/张量指令(Intel AMX)。使用这些高级指令的软件往往在性能上有几个数量级的提升。

随着深度学习领域的快速发展,业界对其他数字格式变量驱动的显著性能提升重新产生了兴趣。研究表明,在使用更少的位来表示变量、节省算力和内存带宽方面,深度学习模型表现同样出色。因此,除了用于算术运算的传统32位和64位格式外,一些CPU特许经营组织最近还往ISA中添加了对诸如8位整数(int8,如Intel VNNI)、16位浮点数(fp16、bf16)之类低精度数据类型的支持。

3.2 流水线

流水线是加快CPU速度的基础性技术,其中多条指令在执行过程中可以重叠。CPU中流水线的灵感源自汽车装配线。指令的处理分为几个阶段,这些阶段并行运行,处理不同指令的不同部分。DLX是由文献(Hennessy&Patterson,2011)定义的简单五阶段流水线,它由以下部分构成:

1.取指(Instruction Fetch,IF)。

2.译码(Instruction Decode,ID)。

3.执行(EXE)。

4.访存(MEM)。

5.回写(Write Back,WB)。

图7展示了这个五阶段流水线CPU的理想流水线视图。在第一个时钟周期,指令 x 进入流水线的取指(IF)阶段。在第二个时钟周期,指令 x 进入译码(ID)阶段,同时程序的下一条指令进入取指(IF)阶段,依此类推。当整个流水线满载时,如图7中第5个时钟周期所示,CPU的所有流水线阶段都忙于处理不同的指令。如果没有流水线的话,指令 x +1就需要等到指令 x 执行完毕才能开始执行。

很多现代CPU都是深度流水线化的,也被称为超级流水线。流水线CPU的吞吐量定义为单位时间内完成和退出流水线的指令数。任何给定指令的延迟是指经过流水线所有阶段的总时间。由于流水线的所有阶段都链接在一起,因此每个阶段都必须准备好同步移动到下一指令。将指令从一个阶段移动到另一个阶段所需的时间定义为CPU的基本机器周期或时钟。流水线运行的时钟周期数通常由流水线最慢的阶段决定。CPU硬件设计人员努力平衡一个阶段可以完成的工作量,因为这直接决定了CPU的运行频率。增加频率可以提高性能,并且通常需要协调和重整流水线以消除由最慢流水线阶段引入的瓶颈。

在非常均衡且不会有任何停顿的理想流水线中,流水线机器中每条指令的执行时间由下式给出:

在实际实现中,流水线会引入几个约束,这些约束限制了上述理想模型。流水线冒险(Pipeline Hazards)会妨碍理想的流水线行为,从而导致停顿。这三种冒险分别是结构冒险、数据冒险和控制冒险。幸运的是,程序员不需要应对流水线冒险,在现代CPU中所有类别的冒险都是由硬件处理的。

结构冒险 由资源冲突而导致。在很大程度上,可以通过复制硬件资源(如使用多端口寄存器或存储器)来消除。然而,要消除所有这些冒险,在硅面积和功耗方面成本可能会变得非常高昂。

数据冒险 由程序中数据的依赖关系导致,可以分为三类:

●写后读(Read-After-Write,RAW)冒险要求相关的读取操作在写入操作后执行。当指令 x +1在上一条指令 x 写入某个位置之前读取同一位置时,就会发生这种冒险,从而导致读取错误的值。CPU通过实现从流水线后期阶段到早期阶段的数据转发(称为“旁路”)来减轻与RAW冒险相关的损失。这个想法是,在指令 x 完全完成之前,指令 x 的结果可以转发到指令 x +1。我们看一下这个例子:

寄存器R1存在RAW依赖。如果我们在加法“R0 ADD 1”完成后直接获取值(从EXE流水线阶段),则无须等到WB阶段将该值写入寄存器文件后再获取。“旁路”有助于节约几个时钟周期,流水线越长,“旁路”就越有效。

●读后写(Write-After-Read,WAR)冒险要求相关写入操作在读取操作后执行。当指令 x +1在上一条指令 x 读取某个位置之前写入相同的位置时,就会发生这种冒险,从而导致读取错误的新值。WAR冒险不是真正的依赖关系,可以通过寄存器重命名技术来消除。

它是一种从物理寄存器中抽象逻辑寄存器的技术。CPU通过保留大量物理寄存器来支持寄存器重命名功能。由ISA定义的逻辑寄存器只是一组宽泛的寄存器文件的别名。通过对架构状态的解耦,解决WAR冒险问题很简单,只需要在写入操作时使用不同的物理寄存器即可。例如:

寄存器R0存在WAR依赖。由于有一个大型物理寄存器池,因此我们可以简单地重命名所有从写入操作开始出现的R0寄存器。一旦通过重命名寄存器R0消除了WAR冒险,我们就可以以任何顺序安全地执行这两个操作。

●写后写(Write-After-Write,WAW)冒险要求相关写入操作在写入操作后执行。当指令 x +1在指令 x 写入某个位置之前写入相同位置时,就会发生这种冒险,从而导致顺序错误的写入操作。寄存器重命名技术也可以消除WAW冒险,允许两个写入操作以任何顺序执行,同时保证最终结果正确。

控制冒险 由程序执行流程的变化而导致。它们产生于流水线分支指令和其他更改程序流程的指令。决定分支方向的分支条件在流水线的执行(EXE)阶段才能见分晓。因此,除非消除控制冒险,否则下一条指令的获取不能被流水线化。下一节介绍的动态分支预测和投机执行等技术可用于克服控制冒险。

3.3 利用指令级并行

因为程序中的大多数指令是独立的,所以都适合流水线化和并行执行。现代CPU实现了大量的附加硬件功能来利用这种指令级并行(Instruction Level Parallelism,ILP)。通过与高级编译器技术的协同工作,这些硬件功能可以显著提升性能。

3.3.1 乱序执行

图7中的流水线示例展示了所有指令按顺序在流水线的不同阶段中移动的情况,即按照它们在程序中出现的顺序在流水线中移动。大多数现代CPU都支持乱序(Out-Of-Order,OOO)执行,即顺序指令可以以任意顺序进入流水线的执行阶段,只受指令之间依赖关系的限制。支持乱序执行的CPU仍必须给出相同的结果,就好像所有指令都是按程序顺序执行一样。指令在最终执行时称为退休,其结果在架构状态中是正确和可见的。为了确保正确性,CPU必须按程序顺序让所有指令退休。乱序执行主要用于避免因依赖引起的停顿而导致CPU资源利用率不足的问题,尤其是在下一节描述的超标量引擎中。

图7 简单五阶段流水线示意图

这些指令的动态调度是通过复杂的硬件结构(如记分板)和诸如寄存器重命名之类的技术实现的,以减少数据冒险。记分板硬件用于安排指令按顺序退休并更新所有的机器状态。它跟踪每条指令的数据依赖关系以及可用数据在流水线中的位置,大多数实现都在努力平衡硬件成本与潜在收益。通常,记分板的大小决定了硬件在调度此类独立指令时可以提前多长时间进行。

图8用一个例子详细说明了乱序执行的基本概念。假设由于某些冲突,指令 x +1无法在时钟周期4和5中执行。按序执行的CPU将暂停所有后续指令进入EXE流水线阶段。而在支持乱序执行的CPU中,没有任何冲突的后续指令(例如,指令 x +2)可以进入流水线并完成其执行。所有的指令仍按顺序退休,即指令按程序顺序完成WB阶段。

3.3.2 超标量引擎和超长指令字

大多数现代CPU都是超标量的,也就是说,它们可以在一个时钟周期内发射多条指令。发射宽度是在同一个时钟周期内可以发射的最大指令数。目前CPU的典型发射宽度为2~6。为了保证恰当的平衡,这种超标量引擎还支持多个执行单元和流水线执行单元。CPU还将超标量功能与深度流水线和乱序执行功能相结合,以获取软件给定片段的最大ILP。

图8 乱序执行的概念

图9展示了简单双发射(发射宽度为2)超标量CPU,即在每个时钟周期中,流水线的每个阶段可以处理两条指令。超标量CPU通常支持多个独立的执行单元,以保证流水线中的指令不发生冲突。与图7中所示的简单流水线处理器相比,有多个重复执行单元可以增加机器的吞吐量。

图9 简单双发射超标量CPU流水线示意图

Intel Itanium等架构使用一种称为超长指令字(Very Long Instruction Word,VLIW)的技术,将调度超标量和多执行单元处理器的负担从硬件转移到编译器。它的基本原理是要求编译器选择正确的指令组合使得机器被充分利用,从而简化硬件。编译器可以使用软件流水线、循环展开等技术来发掘更多的ILP机会,因为硬件受制于指令窗口长度的限制,而编译器可以获得全局信息。

3.3.3 投机执行

如3.2节所述,如果指令在分支条件得到确定之前停顿,控制冒险可能会导致流水线中显著的性能损失。硬件分支预测逻辑是一种避免这种性能损失的技术,用于预测分支的可能方向并从预测路径执行指令(投机执行)。

让我们考虑代码清单3中的一个简短代码示例。为了让处理器了解下一步应该执行哪个函数,它需要知道条件a<b是真还是假。在不知道这一点的情况下,CPU会一直等待,直到分支指令的结果确定下来,如图10a所示。

代码清单3 投机执行

图10 投机执行的概念(投机工作用*标记)

通过投机执行,CPU会对分支判断的结果进行猜测,并从所选路径开始处理指令。假设处理器预测条件a<b返回结果为真,那么它不等待分支结果,而是继续进行,投机地调用foo函数(见图10b,投机工作用*标记)。在条件结果得以明确之前,无法提交对机器状态的修改,以确保机器的架构状态永远不受投机执行指令的影响。在上面的示例中,分支指令比较两个标量值,这个过程很快。但实际上,分支指令可能依赖从内存加载的值,这可能需要耗费数百个时钟周期。如果对条件返回结果预测准确,则可以节省很多时钟周期。但是,有时可能预测不准确,此时应该调用函数bar。在这种情况下,投机执行的结果必须被制止和丢弃。这被称为分支预测错误惩罚,我们将在4.8节讨论。

为了跟踪投机执行的进度,CPU支持一种称为顺序重排缓冲区(ReOrder Buffer,ROB)的结构。ROB维护所有指令执行状态,并按顺序让指令退休。只有在其顺序与程序流一致且投机正确时,被投机执行的结果才会被写入ROB并提交到架构寄存器。CPU还可以将投机执行与乱序执行结合起来,并使用ROB同时跟踪投机执行和乱序执行。

3.4 利用线程级并行

前面描述的技术依赖程序中可用的并行性来加快执行速度。此外,CPU还支持利用在CPU上执行的进程或线程之间的并行性的技术。硬件多线程CPU支持专用硬件资源以独立地跟踪CPU中每个线程的状态(也被称为上下文),而不是只跟踪单个线程或进程的状态。这种多线程CPU的主要目的是在线程由于长时延活动(如内存引用)而被阻塞时,以最小的延迟从一个上下文切换到另一个上下文(不会产生保存和恢复线程上下文的成本)。

同步多线程

现代CPU通过支持同步多线程(Simultaneous Multi-Threading,SMT)将指令级并行技术和多线程技术相结合,以最大限度地利用可用硬件资源。来自多个线程的指令在同一个时钟周期内同时执行。从多个线程同时调度指令会增加利用可用超标量资源的概率,从而提高CPU的整体性能。为了支持SMT,CPU必须复制硬件来存储线程状态(程序计数器、寄存器等),跟踪乱序执行和投机执行的资源可以在线程间复制或分段共享。通常情况下,高速缓存资源会在硬件线程之间动态共享。

3.5 存储器层次

为了有效地利用CPU中预置的所有硬件资源,需要在正确的时间向机器提供正确的数据。理解存储器层次对于提高CPU性能至关重要。大多数程序都有局部性的特点,它们不能无差别地访问所有代码或数据。CPU存储器层次划分基于两个基本特性:

时间局部性 特定位置的内存被访问后,很可能在不久的将来相同的位置会被再次访问。理想情况下,我们希望在下次需要这些信息时它们已经被放在高速缓存中。

空间局部性 特定位置的内存被访问后,很可能在不久的将来其周边的位置也需要被访问。这是因为相关数据彼此靠近放置。当程序从内存中读取一个字节时,通常会读取一大块内存(缓存行),因为很可能程序很快就会需要使用这些数据。

本节主要概述现代CPU支持的存储器层次系统及其关键属性。

3.5.1 高速缓存层次

高速缓存是CPU流水线发起任何请求(请求代码或数据)的存储器层次中的第一层级。理想情况下,流水线在具有最小访问延迟和无限缓存时表现最佳。实际上,高速缓存访问时间随其容量的增加而增加。因此,高速缓存被组织为最接近执行单元的小型快速存储块的层次结构,且由更大、更慢的块进行备份。高速缓存层次的特定层级可以专门用于代码(指令缓存,i-cache)或数据(数据缓存,d-cache),也可以在代码和数据之间共享(统一缓存)。此外,高速缓存层次的某些层级可以由特定的CPU专用,而另外一些层级的缓存则可以在CPU之间共享。

高速缓存由多个确定大小的块( 缓存行 )组成。现代CPU中典型的缓存行大小是64字节。最接近执行流水线的高速缓存大小通常在8KiB到32KiB之间。在现代CPU中,层次结构中更远的高速缓存可以在64KiB到16MiB。任何层级的高速缓存的架构都由以下四个属性定义。

3.5.1.1 高速缓存中数据的放置

内存访问请求中的地址可以用来访问高速缓存。在直接映射高速缓存中,给定缓存块的地址只能出现在高速缓存中的一个位置,并且由如下映射函数定义:

直接映射的位置=(缓存块地址)mod(缓存中块数)

在全关联高速缓存中,给定的缓存块可以放置在高速缓存中的任何位置。

介于直接映射和全关联映射之间的是组关联映射。在组关联映射高速缓存中,缓存块被组织成组,通常每组包含2、4或8个缓存块。给定的地址首先映射到一个组,在组内,该地址可以放在该组中的块之间的任何位置。每组有 m 个缓存块的高速缓存称为 m 路组关联高速缓存。组关联高速缓存的计算公式是:

3.5.1.2 在高速缓存中查找数据

m 路组关联高速缓存中的每个缓存块都有一个与其关联的地址标签。此外,该标签还包含诸如标记数据有效与否的有效位之类的状态位。标签还可以包含其他位,以指示访问信息、共享信息等,这些内容将在后面的章节中描述。

图11展示了如何使用流水线生成的地址来查找高速缓存。最低顺序地址位定义了给定块内的偏移量,即块偏移量位(32字节缓存行需5位,64字节缓存行需6位)。组则是基于上述公式使用索引位来选择,一旦组被选定,就可以使用标签位与该组中的所有标签进行比较。如果其中一个标签与传入请求的标签匹配并且设置了有效位,则缓存命中。与该块条目相关联的数据(从高速缓存数据组中读取与标签查找同时进行)被提供给执行流水线。如果标签不匹配,则缓存未命中。

图11 高速缓存查找的地址组织

3.5.1.3 管理缓存未命中

当发生高速缓存未命中时,控制器必须在缓存中选择要替换的块,以分配给导致缓存未命中的地址。对于直接映射高速缓存,由于新地址只能分配在一个位置,因此之前映射在该地址的缓存条目将被释放,并在该位置加载新的条目。在组关联高速缓存中,由于新的缓存块可以放置在缓存组的任何位置块中,因此需要引入一个替换算法。典型的替换算法是最近最少使用(Least Recently Used,LRU)策略,即最近访问次数最少的缓存块被释放,为未命中地址腾出缓存空间。另一种可选算法则随机地选择一个缓存块作为牺牲对象。大多数CPU在硬件层定义这些功能,这样更容易执行软件。

3.5.1.4 管理写操作

对高速缓存的读访问是最常见的情况,因为程序通常读取指令,并且数据读取多于数据写入。在缓存中处理写操作更困难,CPU会使用不同的技术来处理这种复杂情况。软件开发人员应该特别注意硬件支持的缓存写操作的流程,以确保代码性能最佳。

CPU的设计使用两种基本机制来处理高速缓存中的缓存命中写入操作:

在写直达(Write-Through)高速缓存中,命中的数据同时写入缓存块和层次结构中较低的层级。

在回写(Write-Back)高速缓存中,命中的数据只写入缓存。因此,层次结构较低层级中就会包含过期数据。修改后的缓存行的状态通过标签中“脏”标识位来追踪。当修改后的缓存行最终被从缓存中驱逐时,回写操作会强制将缓存行数据写回层次结构的较低层级中。

写入操作时的高速缓存未命中可以通过两种方式处理:

在写入未命中时写分配(Write-Allocate)或读取(Fetch)高速缓存中,未命中位置的数据从层次结构中较低层级加载到高速缓存,随后像写入命中情况一样处理剩余写入操作。

假如高速缓存使用非写分配(no-write-allocate)策略,写入未命中的事务直接被发送到层次结构中所有的较低层级,并且缓存块不会被加载到高速缓存中。

在这些选项中,大多数设计通常选择用写分配策略实现回写高速缓存,因为这些技术都试图将后续写入事务转换成缓存命中情形,而不会将额外的流量发送到层次结构中较低的层级。写直达高速缓存通常会使用非写分配策略。

3.5.1.5 其他高速缓存优化技术

对于程序员来说,理解高速缓存层次结构的行为对于挖掘应用程序的性能至关重要,尤其是在CPU时钟频率不断提高而内存技术速度落后时。从流水线的角度来看,任何访问请求的延迟都可以由以下公式计算出来,该公式可以递归地应用于高速缓存层次的所有层级,直至主存:

平均访问时延=命中花费的时间+未命中比例×未命中花费的时间

硬件设计师通过许多新颖的微架构技术来解决减少命中时间和未命中负面影响的挑战。从根本上讲,高速缓存未命中会使流水线停顿,进而影响性能。对高速缓存而言,未命中比例高度依赖缓存的架构(如块大小、关联性)以及运行在机器上的软件。因此,优化未命中比例变成了一项硬件和软件协同设计的工作。如前所述,CPU为高速缓存提供了最佳的硬件组织结构。接下来将描述在硬件和软件中可以最大限度降低缓存未命中比例的其他技术。

硬件和软件预取技术

减少缓存未命中以及后续停顿的方法之一,就是先于流水线需要将指令和数据预取到高速缓存层次的不同层级。这里假设,如果在流水线中提前发出预取请求,则处理缓存未命中的时间几乎可以忽略。绝大多数CPU都支持基于硬件的隐式预取,并辅以程序员可以控制的显式软件预取。

硬件预取器观察正在运行的应用程序的行为,并基于重复的高速缓存未命中规律启动预取。硬件预取技术可以自动适应应用程序动态行为,例如适应不同的数据集,并且不需要优化编译器或者剖析功能的支持。另外,硬件预取不会有额外的地址生成和指令预取开销。但是,硬件预取技术仅限于学习和预取硬件中实现的一组高速缓存未命中模式。

软件内存预取是对硬件预取的补充,开发者可以通过特定的硬件指令提前指定需要的内存位置(见8.1.2节)。编译器还可以自动将预取指令添加到代码中,以便在数据使用之前请求数据。预取技术需要平衡实际需求和预取请求,以避免预取流量挤压实际需求流量。

3.5.2 主存

主存是存储器层次中的下一层级,位于高速缓存的下游。主存使用支持大容量且成本合适的DRAM(动态RAM)技术。描述主存的三个主要属性是延迟、带宽和容量。延迟通常包含两部分,即内存访问时间和内存周期时间。内存访问时间指请求到数据可用时所消耗的时间,内存周期时间指两个连续的内存访问之间所需的最短时间。

大多数CPU都支持的主流DRAM技术是DDR(Double Data Rate,双倍数据速率)DRAM技术。历史上,DRAM带宽每一代都得到了提升,而延迟保持不变,甚至更高。表2展示了最新三代DDR技术的最高数据速率和对应的延迟,数据速率以每秒百万传输次数(10 6 /s)的单位度量。

表2 最新三代DDR技术的最高数据速率和对应的延迟

新的DRAM技术——例如GDDR(Graphics DDR,图形DDR)和HBM(High Bandwidth Memory,宽带宽内存)——在需要更大带宽的定制处理器上使用,所以不被DDR接口支持。

现代CPU支持多个独立通道的DDR DRAM内存。一般而言,每一个内存通道的宽度是32位或64位。

3.6 虚拟内存

虚拟内存是让所有运行在CPU上的进程可以共享属于该CPU的物理内存的机制。虚拟内存提供了一种保护机制,可以限制其他进程对分配给指定进程内存的访问。虚拟内存还提供了重定位机制,即能将程序加载到物理内存的任意位置而无须改变程序内寻址方式。

在支持虚拟内存的CPU上,程序使用虚拟地址进行访问。虚拟地址由提供虚拟地址和物理地址之间映射的专用硬件表翻译成物理地址,这些表被称作页表。图12展示了地址的翻译机制,虚拟地址被分成两部分,虚拟页编号用于在页表(页表可以是单层的,也可以是嵌套的)中建立索引,从而生成虚拟页编号到对应物理页的映射关系。然后,使用虚拟地址中的页偏移量访问映射后物理页中相同偏移量的物理内存位置。如果请求的页不在主存中,则会导致缺页问题。操作系统负责向硬件提供处理缺页问题的提示信息,这样最近最少使用的页将被交换出去,以便为新页腾出空间。

图12 虚拟地址的翻译机制

CPU通常使用层级结构的页表格式将虚拟地址位有效地映射到可用的物理内存。在这样的系统中,缺页代价是很高的,需要遍历整个层级结构。为了减少地址翻译时间,CPU支持一个称为翻译后备缓冲区(Translation Lookaside Buffer,TLB)的硬件结构来缓存最近使用过的翻译。

3.7 单指令多数据多处理器

另一种多重处理的变种是在特定工作负载上广泛使用的SIMD(Single-Instruction-Multiple-Data,单指令多数据)多处理器,它不同于前面描述的MIMD(Multiple-Instruction-Multiple-Data,多指令多数据)方式。顾名思义,在SIMD处理器中,单条指令通常在单个时钟周期内使用许多独立的功能单元对多个数据元素进行操作。向量和矩阵的科学计算都非常适合SIMD架构,因为向量或矩阵的每个元素都需要使用相同的指令进行处理。SIMD多处理器主要用于数据并行并且只需要少量功能和操作的特殊用途任务。

图13展示了代码清单4中列出的代码的标量执行模式和SIMD执行模式。在传统的标量执行模式—SISD(Single-Instruction-Single-Data,单指令单数据)模式中,加法操作被单独应用到数组a和b的每一个元素上。但是,在SIMD模式中,加法在同一时间被应用到了多个元素上。SIMD CPU支持对向量元素执行不同操作的执行单元,数据元素可以是整数,也可以是浮点数。SIMD架构能更有效地处理大量数据并且适用于有向量操作的数据并行应用程序。

代码清单4 SIMD执行

图13 标量执行模式和SIMD执行模式示例

大多数主流CPU架构都具有向量指令特性,包括x86、PowerPC、ARM和RISC-V。1996年,Intel发布了一个新的指令集MMX,这是一个专为多媒体应用程序设计的SIMD指令集。在MMX之后,Intel又推出了具有新功能和不同向量大小的新指令集:SSE、AVX、AVX2和AVX512。新的指令集一经推出,软件工程师就开始使用它们。起初,新的SIMD指令以汇编的方式编程。后来,引入了特殊的编译器内建函数。如今,所有主要的编译器都支持主流处理器向量化。

3.8 现代CPU设计

图14中的框图展示了Intel第六代核Skylake的细节,该核于2015年公布,已广泛部署到了世界各地。Skylake核被分成一个获取x86指令并将之解码为微操作(u-op)的有序前端和一个8路超标量的无序后端。

图14 Intel Skylake微架构中CPU核的框图[©图片来自文献(Int,2020)]

该核支持2路SMT,有一个32 KB的8路一级指令高速缓存(L1指令缓存)和一个32 KB的8路一级数据高速缓存(L1数据缓存)。其中,L1高速缓存由一个通用的1 MB二级高速缓存(L2高速缓存)备份,并且L1和L2高速缓存都是每个核私有的。

3.8.1 CPU前端

CPU前端由许多数据结构构成,其主要目的是有效地从内存中获取指令并解码。它的主要功能是将准备好的指令送入CPU后端,而后者负责指令的实际执行。

CPU前端在每个时钟周期从L1指令缓存中获取16字节的x86指令,两个线程之间共享这些指令,每个线程每隔一个时钟周期就会得到16个字节。这些是长度可变的复杂x86指令,流水线中的预解码和解码阶段将这些复杂x86指令转换为微操作(见4.4节),这些微操作会排队进入指令解码队列(Instruction Decode Queue,IDQ)。

首先,预解码阶段通过检查指令来确定和标记变长指令的边界。在x86中,指令长度从1字节到15字节不等。此外,该阶段还识别分支指令。预解码阶段将最多移动6条指令(也被称为宏指令)到两个线程共享的指令队列中。指令队列还支持宏指令融合单元,该单元检测两条宏指令是否可以融合成一个微操作(见4.4节)。这种优化可以节省流水线其他部分的带宽。

每个时钟周期最多可以从指令队列向解码器发送5条预解码的指令。两个线程共享该接口,并每隔一个时钟周期各访问一次。然后,5 路解码器将复杂的宏指令转换为固定长度的微操作。

前端的一个主要性能提升特性是解码流缓冲区(Decoded Stream Buffer,DSB)或微操作高速缓存。其目的是将宏指令到微操作的转换缓存在一个单独的结构(DSB)中,其中DSB与L1指令缓存并行工作。在指令获取期间,还会检查DSB以确定微操作转换是否已经在DSB中可用。经常发生的宏指令将在DSB中命中,流水线将避免重复且成本高昂的16字节包的预解码和解码操作。DSB可以提供与前端到后端接口容量相匹配的6个微操作,这有助于保持整个核的平衡。DSB与分支预测单元(Branch Prediction Unit,BPU)协同工作。BPU预测所有分支指令的方向,并根据该预测结果来引导下一条指令的获取。

一些非常复杂的指令需要的微操作可能比解码器所能处理的上限更多。此类指令的微操作由微码序列器只读存储器(Microcode Sequencer Read-Only Memory,MSROM)提供,这类指令的例子包括支持字符串操作、加密、同步等的硬件操作。另外,MSROM保留了微码操作以处理异常情况,例如,分支预测错误(需要流水线刷新)、浮点辅助(比如当指令以非规范浮点数操作时)等。

指令解码队列(IDQ)提供了有序前端和无序后端之间的接口,并按顺序排列微操作。IDQ总共有128个微操作,每个硬件线程有64个微操作。

3.8.2 CPU后端

CPU后端采用乱序(Out-Of-Order)引擎来执行指令并存储结果。

CPU后端的核是一个包含224个条目的顺序重排缓冲器(ReOrder Buffer,ROB)单元,此单元负责处理数据依赖问题。ROB将架构可见的寄存器映射到调度器/预留单元中使用的物理寄存器,ROB还提供寄存器重命名和跟踪投机执行功能,它的条目总是按照程序中的顺序退休。

预留单元/调度器(Reservation Station/Scheduler,RS)是一种用来跟踪给定微操作所有资源可用性的结构,一旦资源准备就绪就将微操作调度到分配的端口。由于核支持8路超标量,因此RS在每个时钟周期最多可以调度8个微操作。如图14所示,每个调度端口支持不同的操作:

端口0、1、5和6提供所有整数、浮点数和向量加法单元,分配到这些端口的微操作不需要做内存操作。

端口2和3用于地址生成和加载操作。

端口4用于存储操作。

端口7用于地址生成。

3.9 性能监控单元

每个现代CPU都提供监控性能的方法,这些方法被集成到了性能监控单元(Performance Monitoring Unit,PMU)中。PMU包含可帮助开发人员分析其应用程序性能的功能,图15给出了现代Intel CPU中的一个PMU示例。大多数现代PMU都有一组性能监控计数器(Performance Monitoring Counter,PMC),用以收集程序运行过程中发生的各种性能事件。5.3节将讨论如何使用PMC进行性能分析。此外,还有一些其他增强性能分析的功能,例如LBR、PEBS和PT,第6章将专门讨论这些功能。

随着每一代CPU设计的不断发展,PMU也在不断发展。使用cpuid命令可以确定CPU中PMU的版本 ,如代码清单5所示。每个Intel PMU版本的特性及其相比之前版本的变化,详见文献(Int,2020)。

图15 现代Intel CPU的性能监控单元(PMU)

代码清单5 查询PMU

性能监控计数器

想象一下处理器的简化视图,它看起来可能就像图16中所示的那样。如前所述,一个现代CPU有高速缓存、分支预测器、执行流水线和其他单元。当连接到多个单元后,PMC可以从它们那里收集有意义的统计数据。例如,它可以统计经过了多少个时钟周期,执行了多少条指令,某段时间内发生了多少次高速缓存未命中或分支预测错误,以及其他性能事件。

图16 带有性能监控计数器的CPU简化视图

一般来说,PMC为48位宽,这使得分析工具可以在不中断程序执行的情况下运行更长时间 。性能计数器是作为模型特定寄存器(Model Specific Register,MSR)实现的硬件寄存器,这意味着PMC的数量和位宽可能因模型的不同而不同,你不能指望CPU中始终有相同数量的计数器。你应该先通过工具—如cpuid—来查询计数器数量和位宽。PMC可以通过RDMSR和WRMSR指令访问,但是这些指令只能在内核空间里执行。

工程师经常想知道执行的指令数量和经历的时钟周期数,因此Intel处理器的PMU有专门的PMC来收集这些事件,PMU提供固定功能计数器以及可编程计数器。固定功能计数器总是测量CPU核内的同一事件,而可编程计数器由用户来选择他们想要测量的事件。通常,每个逻辑核有4个可编程计数器和3个固定功能计数器。固定功能计数器通常被设置为计算核时钟、参考时钟和退休指令(见第4章)。

PMU中存在大量性能事件是很常见的,图15中显示的只是现代Intel CPU中所有监控性能事件中的一小部分。不难发现,可用PMC的数量远小于性能事件的数量。同时统计所有的性能事件是不可能的,不过分析工具在程序执行过程中通过性能事件组之间的多路复用来解决这个问题(见5.3.3节)。

文献(Int,2020)给出了Intel CPU的全部性能事件。对于ARM芯片,还没有做这样严格的定义。每个ARM供应商按照ARM架构实现核,但是性能事件的含义和所支持的性能事件各不相同。 DzzQWeuZg2SOuyC5Z7xbFJa7WqD6k+jiOP9uqZNU+6pyXJD1oYWLodKNNZtjbB0l

点击中间区域
呼出菜单
上一章
目录
下一章
×