本节将介绍一下计算机的多级缓存(L1、L2、L3)架构、多级缓存与主存怎么进行数据交互的,另外会对MESI(缓存一致性协议)做下介绍,为后面的JMM模型、volatile关键字的学习打下基础。
一,现代计算机硬件基本结构
1.总线
上面图中大部分都比较好理解(如果想详细了解的话可以看下计算机操作系统相关的书籍,了解一下冯诺依曼计算机模型等基础知识),这里先介绍一下总线:贯穿整个系统的是一组电子管道,称作总线,它携带信息字节并负责在各个部件间传递,计算机的各个组件如CPU、内存、磁盘等通过总线进行通信。通常总线被设计成传送定长的字节块,也就是字(word)。字中的字节数(即字长)是一个基本的系统参数,各个系统中都不尽相同。现在的大多数机器字长要么是4个字节(32位),要么是8个字节(64位)。
2.CPU
中央处理单元-CPU:又分为三部分,分别是控制单元、存储单元、运算单元
控制单元:控制单元是整个CPU的指挥控制中心,由指令寄存器IR(Instruction Register)、指令译码器ID(Instruction Decoder)和 操作控制器OC(Operation Controller) 等组成。它根据用户预先编好的程序,依次从存储器中取出各条指
令,放在指令寄存器IR中,通过指令译码(分析)确定应该进行什么操作,然后通过操作控制器OC,按确定的时序,向相应的部件发出微操作控制信号。
运算单元:运算单元是运算器的核心。可以执行算术运算和逻辑运算。相对控制单元而言,运算器接受控制单元的命令而进行动作,即运算单元所进行的全部操作都是由控制单元发出的控制信号来指挥的,所以它是执行部件。
存储单元:存储单元包括 CPU 片内缓存Cache和寄存器组,是 CPU 中暂时存放数据的地方,里面保存着那些等待处理的数据,或已经处理过的数据,CPU 访问寄存器所用的时间要比访问内存的时间短。 寄存器是CPU内部的元件,寄存器拥有非常高的读写速度,所以在寄存器之间的数据传送非常快。采用寄存器,可以减少 CPU 访问内存的次数,从而提高了 CPU 的工作
速度。
二,多级CPU缓存架构
1.多级CPU缓存
由于CPU的运算速度远大于内存的读写速度,所以为了解决两者速度不匹配的问题,CPU缓存出现了。现在CPU大多数情况下读写都不会直接访问内存,取而代之的是CPU缓存,CPU缓存是位于CPU与内存之间的临时存储器,它的容量比内存小得多但是交换速度却比内存快得多。而缓存中的数据是内存中的一小部分数据,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可先从缓存中读取,从而加快读取速度。
Cache(三级缓存),分为两种,早期的是外置,以后的升级产品都是内置的。CPU缓存通常分成了三个级别:L1
,L2
,L3
。级别越小越接近CPU,所以速度也更快,同时也代表着容量越小。L1 是最接近CPU的, 它容量最小(例如:32K
),速度最快,每个核上都有一个 L1 缓存,L1 缓存每个核上其实有两个 L1 缓存, 一个用于存数据的 L1d Cache(Data Cache),一个用于存指令的 L1i Cache(Instruction Cache)。L2 缓存 更大一些(例如:256K
),速度要慢一些, 一般情况下每个核上都有一个独立的L2 缓存; L3 缓存是三级缓存中最大的一级(例如3MB或者6MB),同时也是最慢的一级, 在同一个CPU插槽之间的核(即一个CPU内的不同的核)共享一个 L3 缓存。
每个计算机系统中的存储设备都被组织成了一个存储器层次结构,如下图所示。在这个层次结构中,从上至下,设备的访问速度越来越慢、容量越来越大,并且每字节的造价也越来越便宜。存储器层次结构的主要思想是上一层的存储器作为低一层存储器的高速缓存。因此,寄存器文件就是L1的高速缓存, Ll是L2的高速缓存, L2是L3的高速缓存,L3是主存的高速缓存,而主存又是磁盘的高速缓存。
当CPU要读取一个数据时,首先从一级缓存中查找,如果没有再从二级缓存中查找,如果还是没有再从三级缓存中或内存中查找。一般来说每级缓存的命中率大概都有80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取。
2.带有高速缓存的CPU执行计算的流程
-
程序以及数据被加载到主内存
-
指令和数据被加载到CPU的高速缓存
-
CPU执行指令,把结果写到高速缓存
-
高速缓存中的数据写回主内存
注意:上述步骤4中高速缓存中的数据写会主存并不是立即执行的。
缓存刷回主存的时机:1.缓存满了,采用先进先出或者最久未使用的顺序刷回主存,2.#Lock信号,mesi缓存一致性协议,明确要求数据计算完成后要立马同步回主存
3.缓存行(Cache line)
缓存行 (Cache Line) 便是 CPU Cache 中的最小单位,CPU Cache 由若干缓存行组成,一个缓存行的大小通常是 32或者64 字节(这取决于 CPU),并且它有效地引用主内存中的一块地址。并且始终在第 32 个字节或第 64 个字节处对齐。这样,当 CPU 访问相邻的数据时,就不必每次都从内存中读取,提高了速度。 因为访问内存要比访问高速缓存用的时间多得多。
以64字节大小的缓存行为例,一个 Java 的 long 类型是 8 字节,因此在一个缓存行中可以存 8 个 long 类型的变量。以一个一级缓存总容量为32Kbyte的寄存器来说,如果每一个缓存行定义的大小为64byte,那么整个寄存器就有512个“缓存行”。如果进行对象地址的换算,一个支持64位寻址长度计算机系统中,可以使用8个byte指向一个明确的内存地址起始位,也就是一个缓存行理论上最多可以存储8个对象的内存起始位置;如果再进行长整型换算(长整型为64位,也就是8个byte),那么可以得到一个缓存行可以存储8个长整型的数值。
缓存行会引发伪共享问题,有兴趣可以查阅相关资料看下。
4.CPU缓存与主存的数据交互
CPU读取存储器数据过程
- CPU要取寄存器XX的值,只需要一步:直接读取。
- CPU要取L1 cache的某个值,需要1-3步(或者更多):把cache行锁住,把某个数据拿来,解锁,如果没锁住就慢了。
- CPU要取L2 cache的某个值,先要到L1 cache里取,L1当中不存在,在L2里,L2开始加锁,加锁以后,把L2里的数据复制到L1,再执行读L1的过程,上面的3步,再解锁。
- CPU取L3 cache的也是一样,只不过先由L3复制到L2,从L2复制到L1,从L1到CPU。
- CPU取内存则最复杂:通知内存控制器占用总线带宽,通知内存加锁,发起内存读请求,等待回应,回应数据保存到L3(如果没有就到L2),再从L3/2到L1,再从L1到CPU,之后解除总线锁定。
三,MESI缓存一致性协议
1.缓存一致性问题
在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性(CacheCoherence)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?
解决缓存一致性问题有两种方式(汇编lock指令的两种实现方式):一种是总线加锁,二是缓存一致性协议(缓存行加锁)。
1.总线加锁:早期的CPU使用这种方式,如果CPU1要访问内存的一段逻辑空间时,首先要对总线加锁,这样其他的CPU就不能读写该段内存空间,由加锁的CPU独享该段内存空间,总线锁定把CPU和内存的通信给锁住了,这样严重影响了运行的性能。后期的CPU大部分情况下都不会使用这种方式了。
2.缓存一致性协议(缓存行加锁):需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI、MOSI、Synapse、Firefly及DragonProtocol等等,但是用的最多的就是MESI。缓存一致性协议会锁缓存行,其性能要比锁总线要高得多。
目前的CPU为了解决缓存一致性问题,基本上都会去实现缓存一致性协议。
2.指令重排序问题
为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该
结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化。
3.MESI缓存一致性协议
MESI 是指缓存行的四种状态的首字母。每个Cache line有4个状态,可用2个bit表示,它们分别如下:
状态 | 描述 | 监听任务 |
M 修改 (Modified) | 该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 | 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。 |
E 独享、互斥 (Exclusive) | 该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。 | 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。 |
S 共享 (Shared) | 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 | 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。 |
I 无效 (Invalid) | 该Cache line无效。 | 无 |
备注:
- 1.MESI协议只对汇编指令中执行加锁操作(汇编lock指令)的变量有效,表现到java中为使用volatile关键字定义变量或使用加锁操作
- 2.对于汇编指令中执行加锁操作的变量,MESI协议在以下两种情况中也会失效:
一、CPU不支持缓存一致性协议。
二、该变量超过一个缓存行的大小,缓存一致性协议是针对单个缓存行进行加锁,此时,缓存一致性协议无法再对该变量进行加锁,只能改用总线加锁的方式。
通过一个示例,来说明一下MESI协议的工作过程:
-
当 CPU1 从主内存中取共享变量 X=2 时,通过上述 CPU读取数据的过程将数据读取到 CPU 缓存 L1 中,此时只有 CPU1 缓存行得到共享变量X,X 的状态则为 E,CPU1 的缓存行会时刻的监听嗅探 bus总线
-
当 CPU2 也从主内存中取共享变量 X=2 时,CPU1 的缓存行会嗅探到CP2 对共享变量X 的操作,此时 CPU1 和 CPU2 的缓存行中 X 的状态会变成 S
-
当 CPU1 对 X 进行+1 操作时,寄存器将 L1 cache 的数据读取,并对 X+1 ,将 X=3 赋值给缓存行 L1 cache,L1 cache 的 X =3 通知 L2 cache,L2 cache 再通知 L3 cache ,最终 CPU 缓存上的 X 都是 3,然后 X=3 将会写到主内存中(X=3写回到主存之前需要锁缓存行)这个回写的过程,CPU1 的 X 状态 会变成 M ,而 CPU2 的缓存行通过 bus 总线嗅探到 主内存X 变量的值改变了,此时 CPU2 的 X 状态就会变成 I ,即该缓存行无效。
-
CPU1 缓存行 X 的值回写到主内存后,CPU1 缓存行的 X 状态又会变成 E。注意:这里CPU1中X值修改后,不会立马同步回主存,会等待一个某一个时间点。
-
CPU2 如果又要访问变量X,则CPU1、CPU2的缓存行又会变成S。
注意:这里锁缓存(Cache Locking)就是用了Ringbus + MESI协议。MESI大致的意思是:若干个CPU核心通过ringbus连到一起。每个核心都维护自己的Cache的状态。如果对于同一份内存数据在多个核里都有cache,则状态都为S(shared)。一旦有一核心改了这个数据(状态变成了M),其他核心就能瞬间通过ringbus感知到这个修改,从而把自己的cache状态变成I(Invalid),并且从标记为M的cache中读过来。同时,这个数据会被原子的写回到主存。最终,cache的状态又会变为S。这相当于给cache本身单独做了一套总线(要不怎么叫ring bus),避免了真的锁总线。
如果同一时间两个CPU都要修改变量X,那么两个CPU的变量X有写回到主存时都去要锁缓存行,这时在一个指令周期之内会进行裁决,裁决后只有一个会成功,另外一个会变为无效(这里并不一定会从主从load最新的值,要看后面的指令触发从内存里面load值才会进行load)。