一、缓存Cache
我们看到CPU参数经常会提到1、2、3级缓存容量,有时还有L1$的标识,这是什么呢。
TLB与两级缓存
资料来源:互联网
缓存(Cache)是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,通常缓存不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。
缓存的工作原理是当CPU要读取一个数据时,首先从CPU缓存中查找,找到就立即读取并送给CPU处理;没有找到,就从速率相对较慢的内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。
正是这样的读取机制使CPU读取缓存的命中率非常高(大多数CPU可达90%左右),也就是说CPU下一次要读取的数据90%都在CPU缓存中,只有大约10%需从内存读取。这大大节省了CPU直接读取内存的时间,也使CPU读取数据时基本无需等待。
总体而言,CPU读取数据的顺序是先缓存后内存。
一级缓存即L1 Cache,因为Cache的发音与英语现金Cash的发音相同,因此有时写做L1$。一级缓存分指令和数据,二级缓存即L2 Cache,不分指令和数据。一级缓存是每个核独有,少量多核如12核以上二级缓存是4个或多个核共享,大部分二级缓存是每个核独有的,三级缓存即L3 Cache是通过总线与内核连接的,是多个核共用的。
因为缓存使用SRAM,其晶体管密度低,所占面积大,这就意味着成本高,简单地说,缓存用的越多,成本就越高,性能就越强。Cache存储数据是固定大小为单位的,称为一个Cache entry,这个单位称为Cache line或Cacheblock。给定Cache容量大小和Cache line size的情况下,它能存储的条目个数(number of cache entries)就是固定的。因为Cache是固定大小的,所以它从DRAM获取数据也是固定大小。对于X86来讲,它的Cache line大小与DDR3、4一次访存能得到的数据大小是一致的,即64Bytes。
通常L1 Cache离CPU核心需要数据的地方更近,而L2 Cache则处于边缓位置,访问数据时,L2 Cache需要通过更远的铜线,甚至更多的电路,从而增加了延时。
L1 Cache分为ICache(指令缓存)和DCache(数据缓存),指令缓存ICache通常是放在CPU核心的指令预取单元附近的,数据缓存DCache通常是放在CPU核心的load/store单元附近。而L2 Cache是置于CPU pipeline之外的。还有L3缓存,通常是多核共享的缓存,容量更大。
缓存是逐级的,1级缓存中找不到,就到2级缓存里找,然后是3级,最后是芯片外的存储器。 由于内存种类多,需要引入内存管理单元,即MMU。MMU是硬件设备,它被保存在主存(main memory)的两级页表控制,MMU的主要作用是负责从CPU内核发出的虚拟地址到物理地址的映射,并提供硬件机制的内存访问权限检查。MMU使得每个用户进程拥有自己的地址空间,并通过内存访问权限的检查保护每个进程所用的内存不被其他进程破坏。
处理器引入MMU后,读取指令、数据需要访问两次内存:首先通过查询页表得到物理地址,然后访问该物理地址读取指令、数据。为减少因MMU导致的处理器性能下降,引入了TLB,TLB是Translation Lookaside Buffer的简称,可翻译为“地址转换后缓冲器”,亦简称为“快表”。简单地说,TLB就是页表的Cache,其中存储了当前最可能被访问到的页表项,其内容是部分页表项的一个副本。只有在TLB无法完成地址翻译任务时,才会到内存中查询页表,这样就减少了页表查询导致的处理器性能下降。
什么是页表(Page Table)
这是操作系统里的一个术语,操作系统的一个主要任务是将程序彼此隔离。因此需要建立不同的内存空间,也就是需要给内存安排不同的地址。页表通常是操作系统决定的。对于32位操作系统,如果想支持32位的操作系统下的4GB进程虚拟地址空间,假设页表大小为4K,则共有2的20次方页面。
如果采用速度最快的1级页表,对应则需要2的20次方个页表项。一个页表项假如4字节,那么一个进程就需要(1048576*4=)4M的内存来存页表项。这太大了,成本太高,因此需要分级,采用2级页表,则创建进程时只需有一个页目录就可以了,占用(1024*4)=4KB的内存。剩下的二级页表项只有用到的时候才会再去申请。若是64位,则需要4级页表,Linux在v2.6.11以后,最终采用的方案是4级页表,分别是:
PGD:Page Global Directory (47-39), 页全局目录
PUD:Page Upper Directory (38-30),页上级目录
PMD:Page Middle Directory (29-21),页中间目录
PTE:Page Table Entry (20-12),页表项
对于任何一条带有地址的指令,其中的地址应该认为是虚拟内存地址而不是物理地址。假设寄存器a0中是地址0x1000,那么这是一个虚拟内存地址。虚拟内存地址会被转到内存管理单元(MMU,Memory Management Unit)并翻译成物理地址。之后这个物理地址会被用来索引物理内存,并从物理内存加载,或者向物理内存存储数据。从CPU的角度来说,一旦MMU打开了,它执行的每条指令中的地址都是虚拟内存地址。为能够完成虚拟内存地址到物理内存地址的翻译,MMU会有一个表单,表单中,一边是虚拟内存地址,另一边是物理内存地址。
分页技术的核心思想是将虚拟内存空间和物理内存空间视为固定大小的小块,虚拟内存空间的块称为页面(pages),物理地址空间的块称为帧(frames),每一个页都可以映射到一个帧上。每个page创建一条表单条目,所以每一次地址翻译都是针对一个page。而RISC-V中,一个page是4KB,也就是4096Bytes。对于虚拟内存地址,我们将它划分为两个部分,index和offset,index用来查找page,offset对应的是一个page中的哪个字节。当MMU在做地址翻译的时候,通过读取虚拟内存地址中的index可以知道物理内存中的page号,这个page号对应了物理内存中的4096个字节。之后虚拟内存地址中的offset指向了page中的4096个字节中的某一个,假设offset是12,那么page中的第12个字节被使用了。将offset加上page的起始地址,就可以得到物理内存地址。
页表是存在内存里的。那就是一次内存I/O光是虚拟地址到物理地址的转换就要去内存查4次页表,再算上真正的内存访问,最坏情况下需要5次内存I/O才能获取一个内存数据,这太费时间,也导致功耗增加,于是TLB诞生了,TLB就是页表缓存。
当CPU执行机构收到应用程序发来的虚拟地址后,首先到TLB中查找相应的页表数据,MMU从TLB中获取页表,翻译成物理地址,如果TLB中正好存放着所需的页表,则称为TLB命中(TLB Hit),接下来CPU再依次看TLB中页表所对应的物理内存地址中的数据是不是已经在一级、二级缓存里了,若没有则到内存中取相应地址所存放的数据。如果TLB中没有所需的页表,则称为TLB失败(TLB Miss),接下来就必须访问物理内存中存放的页表,同时更新TLB的页表数据。 TLB也分指令和数据。
典型架构Cortex-A78缓存指令流程
图片来源:ARM
MOP缓存是一些经过预处理指令的融合操作缓存。
二、超标量
早期的计算机都是串行计算,随着对吞吐量需求越来越高,并行计算出现了。常见的并行计算有三种指令并行、数据并行和任务并行。任务并行更多借助软件才能实现。对硬件来说有指令并行(ILP)、线程并行和数据并行三种。
指令级并行是一种隐式并行,对用户来说是完全透明的,用户完全看不到也感觉不到,也就是写程序的人不需要关注,通过流水线和超标量,使得一个程序的指令序列中有多条同时乱序运行,顺序提交。这依赖寄存器重命名、多个执行单元、重排序缓冲和指令预测技术。
线程级并行是一种显式并行,对用户来说不透明,也就是说程序员要写多线程程序。线程级并行主要指同时多线程(SMT)或超线程(HT)以及多核和多处理器。SMT是在指令级并行的基础上的扩展,可以在一个核上运行多个线程,多个线程共享执行单元,以便提高部件的利用率,提高吞吐量。SMT需要为每个线程单独保持状态,如程序计数器(PC)、寄存器堆、重排序缓冲等。 数据级并行是一种显式并行,主要指单指令多数据(SIMD),比如a,b和c都是相同大小的数组,要进行的计算是a的每一个元素与b的响应元素进行运算,结果放入c的对应元素中。如果没有SIMD,就需要写一个循环执行多遍来完成,而SIMD中一条指令就可以并行地执行运算。
线程的概念就是程序的执行序,每个执行序有执行上下文需要保存。 传统的通用处理器都是标量处理器,一条指令执行只得到一个数据结果。但对于图像、信号处理等应用,存在大量的数据并行性计算操作,这些数据都具备方向,一般称为向量计算。
超标量(Superscalar)是指在CPU中有一条以上的流水线,并且每时钟周期内可以完成一条以上的指令,这种设计就叫超标量技术。其实质是以空间换取时间。而超流水线是通过细化流水、提高主频,使得在一个机器周期内完成一个甚至多个操作,其实质是以时间换取空间。
超标量CPU的工作流程如下:
(1)Fetch(取指令):这部分负责从I-Cache中取指令,I-Cache负责存储最近常用的指令;分支预测器用来决定下一条指令的PC值。
(2)Decode(解码):识别指令的类型
(3)RegisterRenaming(寄存器重命名):解决WAW和WAR这两种“伪相关性”,需要使用寄存器重命名的方法,将指令集中定义的逻辑寄存器重命名为处理器内部使用的物理寄存器。物理寄存器的个数更多于逻辑寄存器,处理器可以调度更多可以并行执行的指令。将存在RAW的寄存器进行标记,后续通过旁路网络(bypassing network)解决存在的“真相关性”。
(4)Dispatch(分发):被重命名之后的指令会按照程序中的规定顺序,写到发射队列(Issue Queue)、重排序缓存(ROB)和Store Buffer等部件中
(5)Issue(发射):经过流水线的分发(Dispatch)阶段之后,指令被写到了发射队列(Issue Queue)中,仲裁(select)电路回从这个部件中挑选出合适的指令送到FU中执行。
(6)RegisterFile Read(读取寄存器):被仲裁电路选中的指令需要从物理寄存器堆(Physical Register File,PRF)中读取操作数
(7)Execute(执行):各种FU单元执行
(8)Writeback(写回):将FU计算的结果写到物理寄存器(PRF)中,通过旁路网络将计算结果送到需要的地方。
(9)Commit(提交):这个阶段起主要作用的部件是重排序缓存(ROB),它将乱序执行的指令拉回到程序中规定的顺序。
目前绝大多数CPU都是超标量结构。
ARMNeoverse V1的微架构
图片来源:ARM
上图是ARM Neoverse V1的架构,是目前ARM面向服务器领域最强的计算架构。对于一个内核架构来说,通常分为三部分:前端、执行引擎和存储系统,其中前端是着墨最多的地方,也是变化最多的地方。
图中解码器为5路,V1架构最高可扩展到8路,也就是IPC为8。这是目前最高的IPC。ARM架构的CPU增加性能最简单的方法就是提高IPC。
每增加一路解码器,系统的复杂程度就会大大增加,在内核前端部分,解码器所占的面积最大,也就是占芯片成本比例最高,要保障成本不大幅度增加同时提升性能,就需要先进的芯片制造工艺,尽量提高晶体管密度,在有限的面积内塞入更多的晶体管。ARM架构的升级和芯片制造工艺的升级是相辅相成的,没有先进制造工艺,ARM架构的芯片性能提升也带来成本大幅度提升,没有ARM架构的升级,先进制造工艺也无用武之地。
流水线是为了提高时钟频率,将每个步骤再细分,分的越细,每一级执行的时间就越短,运行频率就可以提高。
但流水线并非越深越好。
(1)每一级流水线均由寄存器组成,更多的流水线级数意味着消耗更多的寄存器,产生更大的面积开销,功耗也会增大,成本也会上升。
(2)每一级流水线需要握手,流水线的最后一级反压信号可能会一直串扰到最前一级造成严重的时序问题。
(3)流水线的取指阶段得知条件跳转的结果是到底跳还是不跳,因此只能进行预测,而到了流水线的末端才能通过实际运算得知该分支是真的该跳还是不该跳。如果发现真实的结果与预期的结果不一致,意味着预测失败,需要将所有的预取指令全部丢失。流水线越深,则意味着浪费和损失越严重;流水线越浅,则浪费和损失越少。
三、乱序执行
超标量CPU最典型特性就是乱序执行,Out-of-Order Execution,简称OoOE或OOE。超标量CPU会处理多种算法,因为不同指令的运算周期数不同,例如乘法运算所需的时间周期就比加法运算长很多,所以如果一个需要很长时间的指令后面紧跟着需要使用其结果的指令的话,就会浪费很多时间,所以打乱指令的执行顺序就很重要。
针对乱序执行有不少的算法,但是归根到底都是以保留栈为基础进行设计的。保留栈的核心思想,是把译码完成后的指令根据各自的指令种类,将译码后的指令送往各自的保留栈中保存下来,如果该指令所有操作数都已准备齐全,则可以开始进行乱序发射。注意,执行可以乱序,但提交不可以,否则就出现错误了。
在处理器中,先后执行的指令之间经常具有相关性(例如后一条指令用到前一条指令向寄存器写入的结果),因此早期简单的处理器使后续指令停顿,直到其所需的资源已经由前序指令准备就绪。
Tomasulo算法则通过动态调度的方式,在不影响结果正确性的前提下,重新排列指令实际执行的顺序(乱序执行),这种算法是1966年Robert Tomasulo 发明,沿用至今。该算法使用了寄存器重命名机制。
指令之间具有数据相关性(例如后条指令的源寄存器恰好是前条指令要写入的目标寄存器),进行动态调度时必须避免三类冒险(即流水线停顿):写后读(Read-after-Write, RAW)、写后写(Write-after-Write, WAW)、读后写(Write-after-Read, WAR)。
第一种冒险也被称为真数据相关(True Data Dependence),而后两种冒险称为伪相关,伪相关并不一定导致流水线停顿,它们可以由寄存器重命名来予以解决,乱序执行的关键就是识别伪相关。 寄存器重命名有两种,一种就是将一体系架构寄存器ARF(Architected Register File)动态映射到一个专门的物理寄存器堆PRF(Physical Register File)上,消除WAW和WAR冒险。
由一个Mapping Table负责管理和查询,这个重命名的过程对用户透明,用户只能看到ARF。读出指令和动作后先把这些都保存在保留站(Reservation Stations)中,然后CDB(公共数据总线)上进行广播,看这个计算结果有没有作为是其他指令源操作数的。如果保留站发现有,就会更新保留站源操作数的值。这样就代替了流水线寄存器。这种办法有可能出现不按顺序提交,因此无法实用。八十年代后对算法做了改进,增加了ROB(Reorder Buffer),在Tomasulo算法把指令分为Issue,Execute, 和Write Result三步的基础上,增加一步,称为Commit(交付,后提交)。
Commit的功能是指令将其结果交付给(写入)目的寄存器或存储单元。必须增加一硬件缓冲存储器(buffer),供Write Result这一步存放已获得的结果,并可以提供给其它指令应用这些结果。当指令进入Commit这一步时,将结果从buffer中拷贝到目的寄存器或存储单元。这一硬件缓冲存储器称为重排序缓冲(ROB,Reorder Buffer)。ROB保证指令顺序发射(issue/dispatch),乱序执行(execute),顺序提交(commit)。通过Reorder buffer(ROB),可以实现Precise Exception和HW Speculation,同时由于ROB保证指令顺序提交,顺便也消除了WAR和WAW 冒险。Precise Exception(精确异常) 的意思是当指令出现异常时(除0、page fault等),前面的指令已经完成,后面的指令不能对寄存器、内存等进行修改,即与顺序执行的效果一样。
乱序执行微架构
图片来源:互联网
ROB示意图,FP指浮点运算。 ROB(重排序缓存)的出现还有一个好处是可以预测式执行,处理器可以根据预测执行指令,因为执行完的指令并未提交,而是放在ROB内,即使出现预测错误,执行了不该执行的指令,但这个指令不会提交,会被直接丢弃。
乱序执行,顺序提交
图片来源:互联网
这当中也有微小区别,一种称之为显式重命名,显式重命名方案中ROB不记录指令的结果,即将提交的数据和处于推测状态的数据都保存在物理寄存器中,因此物理寄存器数目要高于逻辑寄存器数目。采用隐式重命名方案时,ROB (Recorder Buffer) 保存正在执行、尚未提交的指令的结果;ARF (ISA Register File) 保存已经提交的指令中即将写入寄存器中的值。隐式重命名方案中ARF只保存已经提交的指令的值,处于“推测”状态的指令的值由ROB保存,因此需要的物理寄存器数量与逻辑寄存器数量相同。隐式重命名方案还需要建立一个映射表,记录操作数在ROB中的位置。
相比于显式重命名,隐式重命名需要的物理寄存器数目更少,也就意味着成本低,但每个操作数在其生命周期中需要保存在ROB和ARF两个位置,读取数据的复杂度较高、功耗更高。 在英特尔架构中,乱序执行做得异常复杂,引入了Allocator定位器,Allocator管理着RAT (RegisterAlias Table,寄存器别名表)、ROB (Re-Order Buffer,重排序缓冲区)和 RRF (RetirementRegister File,退回寄存器文件)。
RAT将重命名的、虚拟的寄存器(称为Architectural Register 或 Logical Register)指向ROB或者RRF。RAT是一式两份,每个线程独立,每个RAT包含了128个重命名寄存器。RAT指向在ROB里面的最近的执行寄存器状态,或者指向RRF保存的最终的提交状态。
ROB将乱序执行完毕的指令们按照程序编程的原始顺序重新排序的一个队列,以保证所有的指令都能够逻辑上实现正确的因果关系。打乱了次序的指令们(分支预测、硬件预取)依次插入这个队列,当一条指令通过RAT发往下一个阶段确实执行的时候,这条指令(包括寄存器状态在内)将被加入ROB队列的一端,执行完毕的指令(包括寄存器状态)将从ROB队列的另一端移除(期间这些指令的数据可以被一些中间计算结果刷新),因为调度器是In-Order顺序的,这个队列(ROB)也就是顺序的。从ROB中移出一条指令就意味着指令执行完毕了,这个阶段叫做Retire回退,相应地ROB往往也叫做Retirement Unit (回退单元,实际也就是提交单元),并将其划为流水线的最后一部分。
在一些超标量设计中,Retire阶段会将ROB的数据写入L1D缓存(这是将MOB集成到ROB的情况),而在另一些设计里,写入L1D缓存由另外的队列完成。例如,2008年英特尔推出的Nehalem, 这个操作就由MOB (Memory Order Buffer,内存重排序缓冲区)来完成。
英特尔Nehalem微架构前端
图片来源:互联网
Nehalem的128条目的ROB担当中间计算结果的缓冲区,它保存着猜测执行的指令及其数据,猜测执行允许预先执行方向未定的分支指令。在大部分情况下,猜测执行工作良好——分支猜对了,因此其在ROB里产生的结果被标识为已结束,可以立即被后继指令使用而不需进行L1Data Cache 的Load 操作(这也是ROB的另一个重要用处,典型的x86应用中Load操作是如此频繁,达到了几乎占1/3的地步,因此ROB可以避免大量的Cache Load操作,作用巨大)。在剩下的不幸情况下,分支未能按照如期的情况进行,这时猜测的分支指令段将被清除,相应指令们的流水线阶段清空,对应的寄存器状态也就全都无效了,这种无效的寄存器状态不会也不能出现在RRF里面。
分支预测
流水线架构把指令的执行分为了多个阶段,每个单元只负责完成指令执行过程中的一个阶段,而中间结果由专门的流水线寄存器暂存。这样理论上,一条指令的执行假设被分为5个阶段,那么当5个单元同时运行一段时间后,理论上相同时间可以同时执行5条指令,当然这只是最简单的情况,实际的情况要复杂得多。
对于条件跳转指令(汇编层面是JMP等,代码层面例如if),需要等当前的指令执行完才知道结果是true还是false,也就是说,要等待若干个时钟周期后,CPU才可以确定下一条要执行的指令究竟是哪个分支的。
现代CPU都是超过十级的深流水线,这个等待时间太长了,难道这段时间就只能干等着吗?当然不是,这里CPU就会采取「分支预测」的方式,预测下一条要执行的分支指令,并预先执行,如果if执行完后发现和预测的分支一致,那就中大奖了,整个执行阶段一点都没有暂停。但如果悲剧地预测错误,那么这时候必须从取址开始,重新执行另一个分支的指令。
分支预测有两种,一种是BHT(分支历史表),一种是BTB(分支目标缓存)。BHT已经很少人用了,BHT记录分支指令最近一次或几次的执行情况(成功或不成功),并据此进行预测;BHT下依然要计算分支目标,所以判定分支是否成功所需的时间应该大于确定分支目标地址所需的时间,这时BHT方法才有用。BHT记录的是跳转信息,简单点的,可以用1bit位记录,例如1表示跳转,0表示不跳转,而这个表格的索引是指令PC值;考虑在32位系统中,如果要记录完整32位的branch history,则需要4Gbit的存储器,这超出了系统提供的硬件支持能力;所以一般就用指令的后12位作为BHT表格的索引,这样用4Kbit的一个表格,就可以记录branch history了。
BTB将分支成功的分支指令的地址和它的分支目标地址都放到一个缓冲区中保存起来,缓冲区以分支指令的地址作为标识。这个缓冲区就是分支目标缓冲器。BTB用于记录一条分支指令的跳转地址,由于这儿存储的是指令地址,例如32位地址,因此,这个表格就不能做到存储BHT那样多的内容了,如果也支持4K条指令,则需要128Kbit的存储空间,这几乎可以赶上一个L1Cache的容量了,所以BTB一般很小,就32项或者64项。
为了尽量提高分支预测准确度,BTB又被细分为NANO、MICRO和主缓存。NANO和MICRO都很小,只有16或32,服务器级的有96项。主缓存最高有8K的。数据库以及ERP等应用,跳转分支将会跨过很大的区域并具有很多的分支。
上一篇:深入了解汽车系统级芯片SoC:ARM的商业模式与CPU微架构概览
下一篇:使用BLDC电机助力机械扫描激光雷达实现360度视场
推荐阅读最新更新时间:2024-11-12 23:09