搞懂计算机内存模型,Cache, Cache Line, MESI协议,伪共享问题,缓存行对齐等问题。

在了解计算机内存模型之前,首先需要对计算机的基本组成和CPU有一个了解。

计算机的三大核心部件:

CPU(中央处理器): 主要负责运算和执行系统的指令,是计算机的运算核心和控制核心。
Memory(内部存储器):内存,又称主存,临时存放CPU的运算数据,以及与硬盘等外部存储器交换的数据,它是CPU能直接寻址的存储空间,由半导体器件制成。特点是存取速率快。
IO(输入/输出)设备:比如显示器,鼠标,键盘,耳机等设备。

CPU的主要组成部分

一个CPU,一般来说,应当包含以下几个主要部分:

PC:			程序计数器,用于记录当前指令地址(Program Counter)
Registers:		寄存器,暂时存储CPU计算需要用到的数据
ALU:			运算单元	(Arithmetic & Logic Unit)
CU:			控制单元	(Control Unit)
MMU:			内存管理单元 (Memory Management Unit)	
Cache:			高速缓冲存储器

存储器的层次结构

计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。

上面我们说到,内存可以被CPU直接寻址,充当硬盘和CPU之间数据交换的桥梁,在硬盘上的数据在被CPU运算时,数据所经过的过程为

在这里插入图片描述
由于程序运行过程中的临时数据是存放在内存当中的,这时就存在一个问题:

虽然从内存中存取数据要比从硬盘中存取数据的速度快,但是,与CPU的执行速度相比,还是要慢的太多。因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。

为了解决这个问题,在CPU和内存之间,引入了高速缓存,也就是Cache高速缓冲存储器。包括L1缓存,L2缓存,L3缓存。其中L1,L2缓存是CPU多核之间各自独有的,L3缓存是多核共享的。

当程序在运行过程中,会将运算需要的数据从主内存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

至此,存储器的层次结构可以具体分为:
在这里插入图片描述

在上图中,寄存器是速度最快,且容量最小的一块空间, L1缓存仅次于CPU寄存器,容量比寄存器大一些,速度比寄存器低一些,以此类推,从CPU到每一层的IO速度大致为:

Register寄存器<1ns
L1缓存≈1ns
L2缓存≈3ns
L3缓存≈15ns
主内存≈80ns

Cache Line的概念

我们知道Cache的作用是减少CPU访问主存的次数,从而提升IO的效率。方式是在CPU访问主内存数据时,会将数据复制进Cache中,后续直接从Cache中读取即可。

这里需要注意的是,Cache 中的数据是按块读取的,当CPU访问某个数据时,会假设该数据附近的数据以后会被访问到,因此,第一次访问这一块区域时,会将该数据连同附近区域的数据(共64字节)一起读取进缓存中,那么这一块数据称为一个Cache Line 缓存行。缓存系统是以缓存行为单位存储的。目前主流的CPU Cache的Cache Line大小都是64字节

注: 并不是所有数据都会被缓存,比如一些较大的数据,缓存行无法容下,那么就只能每次都去主内存中读取。

linux系统查看cache line size 的方式为: cat /sys/devices/system/cpu/cpu1/cache/index0/coherency_line_size

如下图,假设CPU要读取主存中的X,而Y是与X相邻的数据且他们的大小正好一共是64Byte,那么很有可能CPU会将Y连同X一块读取到缓存中去,后续如果CPU需要读取Y的值,只需从缓存中获取即可。

在这里插入图片描述

MESI缓存一致性协议

因为CPU的三级缓存中, L3缓存是多核之间共享的,但是L1,L2缓存是CPU多核之间各自独有的,那么不同的CPU核心中的缓存,有可能会用到同样的数据,在这种情况下,就要保持不同核心缓存中的数据一致性。 比如:当其中一个核心修改了某个数据时,其他核心中缓存的该数据就要失效。

为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。

目前主流CPU解决该问题的方式就是 MESI协议。除此之外,还有MSI,MOSI,Synapse,Firefly等。

在MESI协议中,每个Cache line有4个状态,可用2个bit表示,分别是:

M(Modified):该缓存行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中;
E(Exclusive):该缓存行数据有效,数据和内存中的数据一致,数据只存在于本Cache中;
S(Shared):该缓存行数据有效,数据和内存中的数据一致,数据存在于很多Cache中;
I(Invalid):该缓存行数据无效;

我们再回过头依据上方的图示来分析一下:

假设CPU的核1中的缓存读取了包含X,Y的缓存行, 此时该缓存行的状态为E;
此时CPU的核2也读取了该缓存行,此时该缓存行的状态为S;
假设核1 修改了X的值, 那么在核1中当前缓存行的状态变为M, 核2中该缓存行的状态变为I;

总线锁

除了上述MESI协议可以解决缓存一致性问题之外, 还可以使用总线锁的方式。将内存与缓存系统之间的总线加LOCK#锁,使得每次只有一个线程可以执行内存数据的读写操作,同样可以避免缓存不一致的问题。

因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。

但是由于在锁住总线期间,其他CPU无法访问内存,会导致效率低下

在这里插入图片描述

Cache Line 的缓存伪共享问题

虽然MESI协议解决了缓存数据一致性的问题,但是还存在着一个问题,就是伪共享问题。

因为缓存是以Cache Line缓存行为单位读取的,那么根据MESI协议,当缓存行中任一数据被修改后,别的CPU核中的该缓存行就会失效。

仍然以上图为例:

我们假设CPU的核1 要用到变量X, 核2要用到变量Y, 而X和Y在同一个缓存行中;
此时CPU的核1 和 核2 中的缓存都读取了同时包含X,Y的缓存行;
假设核1和核2同时在各自的缓存中对X,Y值进行了修改,那么对方;
根据MESI协议,这就会导致两个核互相通知对方去重新读取主存中的数据;

不难发现,如果出现了这种情况,会降低CPU的执行效率。这也就是伪共享问题了。

Cache Line对齐(伪共享问题解决方案)

到这里我们可以知道,伪共享问题是针对同一个Cache Line的。只有当多核或多CPU同时操作各自缓存中的同一个Cache Line中的数据时,才有可能出现伪共享问题。以上例子中,如果X,Y不在同一个Cache Line,那么就不会出现伪共享问题。

那么解决方案就是:可以使用缓存行对齐的方式,使变量不处于同一个缓存行

我们以操作long类型的数据为例:

我们知道一个缓存行的大小是64个字节,而一个long占8个字节,也就是说一个缓存行可以储存8个long类型的数据。

那么如果我们要操作的是两个long类型的变量的话,只需要在这两个数据之间插入7个long类型的变量,即可使这两个数据不处于同一个缓存行。

效果如下:
在这里插入图片描述
Java8中已经提供了官方的缓存行对齐的解决方案。

Java8中新增了一个注解:@sun.misc.Contended 加上这个注解的类会自动补齐缓存行。

需要注意的是此注解默认是无效的,需要在jvm启动时设置-XX:-RestrictContended才会生效。

计算机内存模型

通过以上对存储器层次结构和CPU缓存系统的了解,我们可以得到计算机的内存模型。

计算机的内存模型定义了共享内存系统中多线程程序读写操作行为的规范,通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。

img
当然在计算机的内存模型中,涉及到的不止这些问题,还有CPU乱序执行问题,内存屏障,处理器优化问题等。

这些问题之后再谈。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值