计算机组成原理(四)-CPU的高速缓存
作者:互联网
当我们对各组件有了认识之后,那么我们在认识了CPU直接去访问内存的时候,需要申请总线控制权,而且一个8G的内存我们需要访问的地址也许就高达2的30次方,一次寻址访问拿到数据可能就要近百ns,而在CPU主频普遍高于3GHz的今天,内存无疑成为了拖累CPU的主要部件,而速率较快的SRAM又造价昂贵,所以早在一开始,内存速率开始拖累CPU的时候,就引入了高速缓存的机制。
什么是高速缓存?
我们在购买电脑时,通常查看参数时除了注意CPU的主频及核心数量,也会注意到有个缓存数量和缓存大小,一般分为L1,L2,L3缓存,以下图为例,分别为i7-10700F(左)和i5-10400F,两者价格相差了将近一倍,除了CPU核心数量跟多以外,还有智能高速缓存大小带来的价格差异,智能不智能咱不知道,但是很快很贵就对了。下图中的16MB和12MB就分别代表L3级缓存的大小,而通常大小L1<L2<L3的。
缓存SRAM造价昂贵,所以通常不会太大,但是却带来了巨大的提升,为了弥补两者之间的性能差异,我们能真实地把 CPU 的性能提升用起来,而不是让它在那儿空转,我们在现代 CPU 中引入了高速缓存。
内存中的指令、数据,会被加载到 L1-L3 Cache 中,而不是直接由 CPU 访问内存去拿。在 95% 的情况下,CPU 都只需要访问 L1-L3 Cache,从里面读取指令和数据,而无需访问内存。
缓存的理论支持:局部性原理
什么是局部性原理,做过生产开发或者用过Redis的应该知道,Redis就是主要拿来做缓存使用的,我们会缓存热点数据,下次再请求这些数据就不会去访问数据库了而是直接从缓存中获取,直接返回,这就是局部性原理中的时间局部性。
局部性原理包括时间局部性和空间局部性,在漫长的计算机发展过程中,开发人员发现,当一个数据被访问,那么短时间内这个数据再次被访问的概率很大,并且与他相邻的数据也会很快被访问,所以我们认为CPU会在短时间内集中访问一个区域内的数据,前者就是时间局部性原理,后者就是空间局部性原理。
认识字与字块:为了了解缓存是如何工作的我们需要先了解字与字块。
字节:字节是CPU寻址的最小单位,1字节等于8比特,也就是1byte=8bit,常用大B来表示。
字:是指存放在一个存储单元中的二进制代码组合,一个64位CPU,一个字的大小就是8byte
字块:存储在连续的存储单元中而被看作是一个单元的一组字节,64位CPU中这个字块可能是64byte,也就是8个字组成了一个字块。
那么Cache 的数据结构和读取过程是什么样的?
缓存的加载都是以字块为单位进行加载的,我们其实需要的是内存中的数据,那么缓存小内存大我们如何通过缓存找到我们想要的数据并且能够保证就是能跟内存里对应起来呢?
下面就来说说缓存的映射方式:
①:直接映射
以字块为单位将内存和缓存分割为数组,内存向缓存形成多对一的直接映射关系,内存中的一个字块能够映射到缓存中的索引是确定的,如21 号内存块内容在缓存块中的话,它一定在 5 号缓存块(21 mod 8 = 5)中。
但是这种映射又重新带来一种问题,图中5,13,21,29都能映射到缓存5上,我们怎么确定我要21的时候拿到的不是其他呢?
通常我们会把缓存块的数量设置成 2 的 N 次方,那么内存的字块数量也一定是缓存的二次方倍,如8 个缓存字块,就是 2 的 3 次方,32个内存字块就是 2 的 5 次方,我们在通过内存地址的获取缓存数据时候,只需要拿到段地址的低三位,就能确定缓存索引,再根据字块中的偏移量就能拿到唯一对应的字节,道理就是这么个道理。
那这个字是不是我们想要的,就需要引入另一个概念"组标记"
这个组标记会记录,当前缓存块内存储的数据对应的内存块,而缓存块本身的地址表示访问地址的低 N 位。就像上面的例子,21 的低 3 位 101,缓存块本身的地址已经涵盖了对应的信息、对应的组标记,我们只需要记录 21 剩余的高 2 位的信息,也就是 10 就可以了。
除了组标记信息之外,缓存块中还有两个数据。一个自然是从主内存中加载来的实际存放的数据,另一个是有效位用于存放缓存块中的数据是否有效的。如果有效位是 0,无论其中的组标记和 实际的数据内容是什么,CPU 都不会管这些数据,而要直接访问内存,重新加载数据。
重点来了,整体进行举例,比如一个64位的CPU,内存地址线宽度为40位,也就是说一个内存地址可以有40位,假设一个字块有64B,即64字节,内存大小为8G,缓存大小为8M。
那么也就是说一个33位的内存地址就可以唯一确定内存中的一个字节,高位补0。这33位就应该包括10位的索引(8M*2的10次方=8G),偏移量占6位(2的6次方等于64),这低16位就已经可以唯一确定缓存中的一个位置中的一个字节,再结合剩余17位作为组标记。
所以一个内存地址的访问,就会经历这样 4 个步骤:
1.根据内存地址中段地址(字块地址为段地址)的低位,计算在 缓存中的索引;
2.判断有效位,确认缓存中的数据是有效的;
3.对比内存访问地址的高位,和缓存中的组标记,确认缓存中的数据就是我们要访问的内存数据
4.根据内存地址的 偏移量,从字块中读取希望读取到的字节。
如果在 2、3 这两个步骤中,CPU 发现,缓存中的数据并不是要访问的内存地址的数据,那 CPU 就会访问内存,并把对应的 内存中的数据字块更新到对应的缓存中,同时更新对应的有效位和组标记的数据。
②全相连映射
全相连映射方式比较灵活,主存的各块可以映射到缓存的任一块中,缓存的利用率高,字块冲突概率低,只要淘汰缓存中的某一块,即可调入主存的任一块。但是,由于缓存比较电路的设计和实现比较困难,这种方式只适合于小容量缓存采用。
③组相联映射
组相连映射其实就是直接映射和全相连映射取了一个中间值,内存和缓存按照统一大小进行分组,组间采用直接映射,组内采用全相联映射,也就是说组大小等于缓存大小那就是全相连,组大小等于字块大小那就是直接映射。
高速缓存的替换策略
什么是缓存替换策略呢?就是当高速缓存中没有数据时,需要从主存中载入所需要的数据,但是缓存有可能已经满了,此时就要启动替换策略,也就是满了我要换谁。
高速缓存中常见的替换策略:
- 随机算法:看谁不顺眼换谁
- 先进先出算法(FIFO):谁先进来的谁先走
- 最不经常使用算法(LFU):有空间会记录字块的使用频率,最不经常使用的会被淘汰。
- 最近最少使用算法(LRU):优先淘汰一段时间内没有使用的字快,具体的实现方式可能是一个双向链表,数据每被访问一次就会被替换到链表表头,淘汰总是淘汰链表的末尾。
拓展重点:volatile关键字是如何实现内存可见性的?
Java的相关开发人员一定绕不开多线程,那么多线程一定绕不开volatile关键字。其实内存可见性的困扰一开始可能是没有的,他的来源其实是多核CPU的出现,每个核心都有自己的缓存(L1,L2是在核心里边的),那么我改了你不知道问题就出现了。
volatile 关键字究竟代表什么含义呢?它会确保我们对于这个变量的读取和写入,都一定会同步到主内存里,而不是从 Cache 里面读取,这样就不会出现脏读的情况。
使用多核多缓存那么必然要面临缓存的一致性问题,那么我们就必须要考虑两个问题,一个是告知,也就是我一个核心改了,其他核心要知道才行,第二个就是顺序,也就是事务的串行化,其他核心不止应该知道,更应该按修改顺序知道,并且按顺序加载,防止出现因为接收告知顺序的不同最后结果也不同。所以核心和核心之间应该有一条专门的总线,然后通过总线广播给所有的 CPU 核心。
基于这种广播有人开发出了一种缓存一致性协议:MESI
MESI 协议,是一种叫作写失效(Write Invalidate)的协议。在写失效协议里,只有一个 CPU 核心负责写入数据,其他的核心,只是同步读取到这个写入(参考主从复制)。在这个 CPU 核心写入 Cache 之后,它会去广播一个“失效”请求告诉所有其他的 CPU 核心。
MESI 协议的由来呢,来自于我们对 缓存字块的四个不同的标记,分别是:
M:代表已修改(Modified):数据已经被修改,这是个脏数据
E:代表独占(Exclusive):数据没问题,而且跟别的核心没关系,我可以随意修改
S:代表共享(Shared):各个核心共享,改之前得先告诉其他核心
I:代表已失效(Invalidated):数据已失效,丢弃,从主内存拿就行
在独占状态下的数据,如果收到了一个来自于总线的读取对应缓存的请求,它就会变成共享状态。而在共享状态下,因为同样的数据在多个 CPU 核心的 Cache 里都有。所以,当我们想要更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要求先把其他 CPU 核心里面的缓存字块,都变成无效的状态,然后再更新当前 Cache 里面的数据。那么独占有没有让你想到ThreadLocal呢?
标签:缓存,计算机,映射,字块,内存,数据,CPU,高速缓存 来源: https://blog.csdn.net/MiracleWW/article/details/114710778