chapt1 概览¶
为什么流水线级数不能一直往大了去,这样能极大提高主频:
-
硬件消耗增大,每一级流水线都需要流水线寄存器以及控制逻辑
-
存储器例如Dcache的端口一定要多,为了实现理想流水线的并行,可能在上个访存还没返回就需要发起下次访存,这需要更多端口的支持
-
分支预测惩罚增大,流水线越深,判断分支是否正确的流水线阶段就会越往后延,一旦分支预测失败,可能面临清空(squash)几十条指令
总的来说,流水线过深会导致1.功耗居高不下2.执行效率过低(因为分支预测惩罚)
chapt2. cache¶
cache主要由两部分组成,Tag和Data;Data存储的是一片连续地址的数据,Tag是这片连续数据区域的公共地址(高xx位)
一般情况是以数据的大小表示cache的大小,但是实际上tag也会占一部分大小
名词解释:
Cacheline:一个tag+它对应的一片连续数据区域,一般是64B
- 这片数据区域一般称为cache block或者data block
Cache set:由于index只是地址的一部分,可能出现重复,一个地址可以找到多个cacheline,如图中虚线所示,这些cacheline的集合称为cache set
**Cache way:**有多少way,cache set就对应多少个cacheline,可以把way看做是set大小
cache查找时,index用于快速定位,但是由于index只是地址的一部分,很容易出现冲突(两个不同地址的index可能相同),因此在地址中除了index和偏移。剩下bits的全是tag,这样保证了两个不同地址不可能存在index相同的条件下,tag也相同,也就保证了寻址的唯一性
如图所示访问cache的流程(read):
-
通过index快速定位到tag的某一个line number,看有多少way,将每一个way的tag都和自身比较,如果相同则cache命中
-
若命中则从相应cache block读出具体的数,word number选择cache block中的某一个word(一般是机器字长),Byte则表示在一个word内选择哪个byte
cache的组织形式
注意以下说的仅是cache的读取,写入cache在更后部分单独说
-
直接相联:对于一个数据,cache中只有一个地方可以容纳它
- index相同的都放到同一个地方,因此冲突比较频繁
-
多路组相联:对于一个数据,cache中有多个地方可以容纳它(具体多少个由
way指定)-
为了减少冲突,其实有点像解决哈希冲突的拉链法,在index相同的地方分成多路,比如两路,就是允许同时存在两个index相同但是tag不相同的地址数据存入cache
-
图中展示的是tag和data分开放置的,如果同时访问tag和data就是并行访问;如果先访问tag,根据tag比较结果再去获取对应data,则是串行访问;Cache的访问一般都是处理器中的关键路径,延迟比较高,为了提高主频,常常将cache访问也切成流水线
并行访问:Address Calculation阶段计算出要访存的地址,Disambiguation阶段检查load/store指令之间的相关性,Cache Access阶段并行访问data和tag,使用tag比较的结果输入到way mux进行选择,Result Drive阶段从data block中一整个cacheline选出需要的字节
串行访问:前两个阶段一样,Tag Access阶段进行tag的比较,比较结果流入下一流水线阶段,访问对应的data,然后选择数据;好处是少了一个mux,也不需要随时访问data,只需要访问需要的那一行data,减少了功耗;坏处是多了一个周期
-
简单总结串行访问与并行访问:二者都有优缺点,并行访问功耗较大,频率比较低;串行访问多了一个周期,但是可以通过填充指令来掩盖这一个周期
-
对于超标量处理器来说,如果cache访问位于处理器的关键路径,那么可以并行改串行提高主频,同时通过填充其他指令来掩盖这一个周期
-
对于顺序执行的处理器来说,由于无法进行调度,这个多出来的周期无法掩盖,会造成性能显著降低,使用并行或许是一个好选择
-
-
-
-
全相联:对于一个数据,cache中任何一个地方都可以容纳它
-
因此地址中不再需要index来索引到具体的地方,而是在cache中比较所有的tag,找到比较结果相等的那个cacheline;这其实就是内容寻址的存储器(CAM:Content Address Memory)
-
实际应用时,会把tag放在CAM里面,SRAM存数据,当匹配到CAM的某一行时,SRAM中对应的cacheline会被找到,SRAM可以直接输出数据
-
由于有大量的内容需要进行比较,延迟很高,但是灵活度最大,毕竟什么位置都能放
-
例如TLB和victim cache
-
Cache的3C定理:对于cache来说,命中率直接影响其性能,一般来说有三种情况会导致cache缺失
-
Compulsory:冷启动缺失,第一次访问某个数据一定不在cache
-
Capacity:哪怕是每个数据都在不同的cache set,但是如果不同的数据数量超过了cache的容量,那么也会miss
-
Conflict:针对同一个cache set,如果映射到这个位置的数据数量大于cache set的容量(即多少“路”),也会导致miss
cache的写入
以下所说的cache层次都是:L1cache各个core私有,哈佛结构的指令和数据分开的cache;L2多个core共享,指令和数据在一个cache
L1cache一般分为I-cache和D-cache
一般情况I-cache只读不写,即便有需要自修改指令的情况,也是借助于D-cache来实现:将要改写的指令作为数据写到D-cache,然后将D-cache的内容写到下级存储器如L2-cache。这块存储区域一般是指令和数据共享,同时将I-cache中的内容置为无效。那么处理器再次执行到I-cache的对应PC时,就会触发cache miss,从而到L2-cache的对应PC去取指,也就使用了被修改的指令了
对于D-cache来说,其读和写略有不同,写会涉及到一致性的问题。当执行一条store指令时,如果只向D-cache写入,而不通知它的下级存储器,就会造成同一个地址,在D-cache和其下级存储器中数据不一致的情况,要保持一致性,有几种简单做法
-
Write through(写直达):写D-cache时,同时写其下级存储器
- 缺点:写下级存储器这个速度比较慢
-
Write back(写回):写D-cache时,只是做一个记号,不写其下级存储器,当这个cacheline被替换时才通知下级存储器,这样就减少了写低速存储器的频率,提高速度
- 缺点:一致性管理会更加困难
上述的情况都是假设要写的地址位于D-cache,那么如果不在D-cache,就会触发write miss
-
Non-Write Allocate:一种做法是直接写到下级存储器中,不写到D-cache
-
Write Allocate:先把这个miss的数据读到D-cache,然后再进行修改。如果D-cache中这片区域被写过,那么首先需要将cacheline写回下级存储器,然后才能将需要的数据读到cacheline进行修改;修改之后这里也会有一致性问题,解决的办法也就是上面写的write through和write back
那么你能不能在D-cache找到对应的line,虽然是miss,但我还是往里面写,标记为dirty,然后就回到write through等策略了呢?
- 不能,因为store最多也就一个字,但是write through等都是针对cacheline来的;假设store时在D-cache找了一个对应的line,但是只写了一个字比如四个字节,但是后面写到下级存储器时,替换的是line,就会篡改里面正确的值,因此不行
提高cache的性能
-
写缓存:前面提到D-cache发生缺失时,需要从下一级存储器读取数据,写到选定的cacheline中,如果这个cacheline是dirty,还需要先写回下级存储器;此过程涉及到先写后读下级存储器,到那时L2cache或者一般的物理内存,只有一个读写端口,这就要求写和读是串行执行,然而下级存储器的延时又比较长,这会导致cache miss时处理时间很长。为了解决这种情况,就可以采用写缓存,将dirty的cacheline首先放到写缓存中,等到下级存储器的端口空闲时,将写缓存的数据写回到下级存储器
写缓存相当于是L1D-cache和L2Cache之间的一个缓存,他可以掩盖写数据的延迟,但是也会增加系统设计复杂度。当D-cache miss时,不仅需要从下级存储器中找数据,还需要从write buffer中找数据,并且write buffer中的数据最新
Victim cache
有时候,cache中被提出的数据很可能马上就要使用,比如对于一个两路组相联的D-cache来说,如果一个程序频繁地使用三个数据,这三个数据恰好又位于同一个cache set,那么就会导致某个数据经常被踢出然后又经常被写回cache。那么实际上我们可以用一个空间来暂存被踢出的cache块,这个空间就被称为victim cache,它一般采用**全相联**的结构。
Victim cache与cache中的内容是互斥的,也就是exclusive。如果CPU发起请求时,cache miss了,但是在victim cache中找到了,也可以称作hit。本质上来说,victim cache其实增加了cache的容量。
还有一个类似的思想,Filter cache。其设计思路是,当第一次使用一个数据时,放入该Filter cache。如果再次使用该数据,才真正放入cache。其好处是避免只使用一次的数据进入cache,浪费宝贵的cache容量
超标量处理器的取指令
为了实现一个周期取多条指令,假定一个周期发射n条,每条指令大小为1个字长,最简单的办法就是使得数据块大小为n个字,一次正常取一个数据块,读出所有的字即可。但这是理想情况,所有的指令都位于一个cacheline,如果出现跨cacheline的指令,就无法正常工作了。
但是这种方法还是有可取之处,处理器每个周期取出的指令多于能够解码的指令个数。
chapt7 寄存器重命名¶
程序的不同指令间,存在很多相关性,是指一条指令的执行依赖于另外一条指令的结果,可分为以下相关性
-
数据相关性:寄存器之间
-
WAW
-
WAR
-
RAW(真数据相关)
-
-
存储器数据的相关性:把视角从寄存器冲突转移到存储addr的冲突,同样分为
-
WAW
-
WAR
-
RAW
-
-
控制相关性:分支指令引起的,通过分支预测解决
-
结构相关性:处理器部件/资源冲突
上述相关性只有RAW是真的相关性,其他的WAW和WAR可以通过寄存器重命名解决(WAW和WAR在顺序执行时不是问题,在乱序调度时则会成为问题)
通过此阶段重命名要达到的目的是,解决WAW和WAR冲突,同时保留RAW冲突,使我们在后续只关注RAW真数据依赖冲突,以更好地乱序/并行
RAT(Register Alias Table):重命名映射表,逻辑寄存器号到物理寄存器号的映射
-
SRAM实现:输入地址输出数据
-
CAM实现:CAM是给数据查地址,查询速度极快,功耗极大
- 在CPU一般用于TLB或者发射队列的唤醒逻辑
ROB(ReOrder Buffer):重排序缓存
实现寄存器重命名要考虑的问题
-
什么时候占用一个物理寄存器,这个物理寄存器来自哪里
-
什么时候释放一个物理寄存器,这个物理寄存器去向何处
-
分支预测失败时,如何处理
-
发生异常时,如何处理
一般的寄存器重命名方法有如下三种:
-
将逻辑寄存器扩展来实现寄存器重命名
-
使用统一的物理寄存器来实现
-
使用ROB
https://chsgcxy.github.io/messy_notes/arch/register_rename.html
重命名步骤,对一条指令如 dest = src1 op src2
-
对源寄存器 src1,src2进行重命名:在RAT中查表
-
对dest进行重命名:从freelist get一个空闲物理寄存器,将这个映射关系写入RAT
- 在写入之前要保存原本的RAT映射关系,这是为了省下一些物理寄存器
commit时,对于dest来说有一个
例子:下面这个应该如何重命名