2.4 指令缓存体系
CPU流水线每个阶段可以并行执行不同的指令,这样就可以同时处理多个指令,从而提高处理器的性能。CPU流水线的起点就是从指令缓存(Instruction Cache,简称I-Cache)中获取指令。这些指令通常存储在主存储器中,但由于CPU的运行速度远快于内存的访问速度,因此如果直接从主存获取指令,则将导致CPU大部分时间在等待数据,这就是常说的存储器墙(Memory Wall)问题。为了缓解这个问题,处理器通常会有一级或多级的缓存来存储最近使用或者最常用的指令和数据。多级缓存一般分为三级,分别是L1、L2和L3,一般L3是多个CPU核心共享的,L1和L2是一个CPU核心独享的。
在早期没有多级缓存的时候,CPU只有一级缓存。后来CPU开发厂商提供了一种廉价的缓冲方案,让一个比CPU片上缓存更大的缓存通过插槽的形式安装在主板上。在80386处理器时代,CPU速度和内存速度不匹配,为了能够加速内存访问,芯片组增加了对快速内存的支持,这也是在消费级CPU上第一次出现缓存,也是L1(一级缓存)的雏形。这个缓存是可选的,低端主板并没有它,从而性能受到很大影响,高端主板则带有64KB缓存,甚至128KB缓存。80386处理器时代的Intel在CPU里面加入了8KB的缓存,当时也叫作内部Cache。
L1分为指令缓存和数据缓存(Data Cache,简称D-Cache)。指令缓存是用来存储最近或者最常用的指令的缓存。Intel的Pentium处理器系列和Motorola的68000系列第一次使用了数据和指令分开的缓存体系,Pentium处理器具有两个8KB的高速缓存。后来的处理器都在扩展多级的数据缓存,而基本上没有扩展指令缓存,所以并不是说L2和L3没有区分出指令缓存和数据缓存,而是数据缓存一直在膨胀,CPU采用了分级方式管理它们,某些高端服务器的CPU上甚至出现了L4级别的缓存。
指令缓存通常位于取指单元(Instruction Fetch Unit)或指令译码单元(Instruction Decode Unit)附近,其目的是存储处理器当前执行的指令,以供后续的取指和指令译码阶段使用。通过将指令缓存放置在取指单元或指令译码单元附近,可以最大限度地减少取指的延迟,使得指令能够更快速地被处理器获取和译码。
数据缓存通常位于加载/存储单元(Load/Store Unit)或数据访问单元(Data Access Unit)附近,其主要功能是存储处理器需要访问和操作的数据,包括读取和写入数据。通过将数据缓存放置在距离数据访问单元最近的位置,可以减少对主存(主内存)的访问延迟,提高数据的获取速度和处理效率。
当需要执行一个新的指令时,处理器首先会检查这个指令是否在指令缓存中。如果在,就直接从指令缓存中获取并执行这个指令,这被称为缓存命中(Cache Hit)。如果不在,就需要从主存或更高级的缓存中获取这个指令,并将其存储到指令缓存中,这被称为缓存未命中(Cache Miss)。
上文我们提到了取指单元,取指单元包含以下部件:Instruction Translation Lookaside Buffer(ITLB)、Instruction Prefetcher、Predecode Unit。
● Instruction Translation Lookaside Buffer:ITLB是一种特殊的高速缓存,它存储了最近使用的虚拟地址到物理地址的映射。这种映射是在运行时动态完成的,用于将虚拟内存地址转换为实际的物理内存地址。在现代操作系统中,程序使用的是虚拟地址,因此在访问内存时,需要将虚拟地址转换为物理地址。页表用于存储这种映射关系,但查找页表的过程可能会非常耗时,所以将最近用过的地址映射关系存储在ITLB中,可以加速地址转换的过程。
● Instruction Prefetcher:指令预取器的任务是预测下一个将要执行的指令,并尽可能早地将这个指令从主存储器中取出并放入指令缓存中。通过预先取出指令,可以减少处理器在获取指令时需要等待的时间,从而提高处理器的执行效率。预取策略可以根据处理器的设计和应用的特性来确定,包括简单的顺序预取,以及更复杂的基于分支预测的预取策略。
● Predecode Unit:预译码单元的功能是对指令进行初步的译码,确定指令的类型和长度。在很多处理器中,不同的指令可能有不同的长度,而且指令的类型和操作数也可能在位模式中有不同的位置。预译码单元可以在指令真正被译码和执行之前,先进行一些基础的分析工作,以便更快地进行后续的处理。这种方式可以降低处理器在指令译码阶段的复杂性,从而提高执行效率。
指令缓存通常由一系列的存储单元构成,每个存储单元用于存储一个指令。这些存储单元通常被划分为多个组,每个组中可以有一个或多个条目(entry)。指令缓存中的每个条目通常包含一个指令的地址标签、一个或多个指令的数据,以及一些其他的控制信息(如有效位、脏位等)。其中,地址标签用于判断一个请求的指令是否在缓存中,指令数据就是实际的指令内容,其他的控制信息用于管理缓存的状态。
指令缓存的具体结构和大小取决于具体的处理器设计。例如,有些处理器可能采用直接映射(Direct Mapped)的缓存,每个组只有一个条目,这样的设计简单,但可能导致缓存冲突(Cache Conflict)。有些处理器可能采用全相联(Fully Associative)或者组相联(Set Associative)的缓存,每个组可以有多个条目,这样可以减少缓存冲突,但相应地,缓存的复杂性会提高,成本也会增加。
指令缓存和其他部件的连接:指令缓存通常连接到处理器的前端(Front-End),包括取指和指令译码阶段。在取指阶段,处理器会根据当前的指令地址,从指令缓存中获取相应的指令数据。如果缓存命中,则可以直接进入指令译码阶段;如果缓存未命中,则需要从主存或者更高级的缓存中获取这个指令,并将其存储到指令缓存中。在指令译码阶段,处理器会解析指令的操作码(Opcode)和操作数(Operand),然后将指令分发(Dispatch)到相应的执行单元(Execution Unit)进行执行。
另外,指令缓存还需要与内存管理单元(Memory Management Unit,简称MMU)进行连接。MMU 负责从虚拟地址到物理地址的转换,当缓存未命中时,需要通过 MMU 将指令的虚拟地址转换成物理地址,然后从主存或者更高级的缓存中获取指令。
前面我们提到了前端的概念,论文“Improving the Utilization of Micro-operation Caches in x86 Processors”提到了一种x86处理器经典前端结构,指令缓存、微操作缓存(Micro-op Cache)和循环缓存(Loop Cache)都是构成CPU前端的重要步骤,它们共同协作以优化CPU的性能,如图2-9所示。这些缓存在CPU的前端流水线中发挥作用,其工作过程如下。
图2-9 x86处理器经典前端结构
● 指令缓存:CPU前端流水线的第一步通常是指令获取,这涉及一个称为指令缓存的硬件单元。I-Cache的主要任务是存储最近使用的指令序列,以减少从主内存中获取指令所需的时间。当CPU需要执行一个新指令时,它需要查看I-Cache中是否存在该指令。如果存在(缓存命中),则直接从I-Cache获取指令,这通常比从主内存中获取快得多。如果不存在(缓存未命中),则CPU从主内存中获取该指令,并将其放入I-Cache以备将来使用。
● 微操作缓存:在指令获取和指令译码之间,大部分x86处理器使用一种称为微操作缓存的硬件单元。指令译码阶段的任务是将复杂的、变长的机器语言指令转换为一组更简单的、定长的微操作,然后这些微操作被派发到执行单元进行处理。微操作缓存的作用是存储最近译码的微操作,以避免再次进行译码操作。如果同一指令再次出现,则可以直接从微操作缓存中获取其对应的微操作,从而绕过指令译码阶段。
● 循环缓存:主要用于存储和重复执行代码中的循环。处理器通过识别循环,把循环体中的指令或者微操作放入循环缓存,在循环执行期间,处理器直接从循环缓存取出这些指令或微操作,而无须再去访问指令缓存或者微操作缓存。这有助于减少访问这些缓存的能耗,并减少取指阶段产生的延迟时间。
这个流水线的执行顺序是:首先从指令缓存获取指令,然后译码指令并查看是否在微操作缓存中,最后如果代码中有循环,则循环缓存会在此起作用。这3种缓存的目标都是减少延迟时间和降低能耗,以提高处理器的性能。
在CPU的前端缓存体系中,还阶段性地出现过Trace Cache(追踪缓存),Trace Cache首次在Intel的Pentium 4处理器中出现。在Trace Cache中,一条追踪代表一个基本块(Basic Block)或多个基本块的连续执行序列,这些基本块在静态程序代码中可能并不相邻,但在执行时连续。而且,分支指令的结果已经在追踪中处理,所以追踪可以包含跨越分支的指令。
在Trace Cache中缓存的是译码后的微指令流,可以减少指令的译码时间,由于追踪表示的是一串连续执行的指令,所以使用Trace Cache可以减少分支预测的开销,并提高指令的并行度。
由于Trace Cache需要解决一些复杂的问题(如构建和维护追踪,处理缓存的不连续性和冗余等),因此在之后的Intel Core 2微架构中,Intel并没有继续使用Trace Cache,而是选择使用更传统的指令缓存和微操作队列的结构。这样微操作从指令缓存中获取,译码成微操作,放入微操作队列。这种方法降低了处理的复杂性,同时可以实现高性能。