原创

计算机内存模型

1.内存模型

1.1 概念

为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了 CPU 多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。

1.2 内存模型解决并发问题主要采用两种方式:

  • 限制处理器优化
  • 使用内存屏障

仅仅阅读内存模型的概念可能有点摸不着头脑,举个实际例子,随着硬件的不断升级,计算机处理逻辑会遇到很多问题,例如:缓存一致性问题、处理器器优化的指令重排问题等。

2.CPU 和缓存一致性问题

我们都知道 CPU 和 内存是计算机中比较核心的两个东西,它们之间会频繁的交互,随着 CPU 发展越来越快,内存的读写的速度远远不如 CPU 的处理速度,所以 CPU 厂商在 CPU 上加了一个 高速缓存,用来缓解这种问题。

下图是小编用 CPU-Z 查看的本机硬件参数 硬件参数 一般高速缓存有 3 级:L1,L2,L3,CPU 与内存的交互,就发生了变化,CPU 不再与内存直接交互,CPU 会先去 L1 中寻找数据,没有的话,再去 L2 中寻找,然后是 L3,最后才去内存寻找(更准确的来说,应该是 CPU 中的寄存器去寻找)。我们可以画一张图来理解: 缓存 看起来一切都很美好,但是随着科技的进步,CPU 厂商们叒搞事了,推出了多核 CPU,每个 CPU 上又有高速缓存,CPU 与内存的交互就变成了下面这个样子: 多核

2.1 缓存不一致问题

为什么会出现这个问题呢? CPU 需要修改某个数据,是先去 Cache 中找,如果 Cache 中没有找到,会去内存中找,然后把数据复制到 Cache 中,下次就不需要再去内存中寻找了,然后进行修改操作。 而修改操作的过程是这样的:在 Cache 里面修改数据,然后再把数据刷新到主内存。其他 CPU 需要读取数据,也是先去 Cache 中去寻找,如果找到了就不会去内存找了。 所以当两个 CPU 的 Cache 同时都拥有某个数据,其中一个 CPU 修改了数据,另外一个 CPU 是无感知的,并不知道这个数据已经不是最新的了,它要读取数据还是从自己的 Cache 中读取,这样就导致了“缓存不一致”。

2.2 解决缓存不一致的方法

解决缓存不一致的方法有很多,比如: 总线加锁(此方法性能较低,现在已经不会再使用)MESI 协议: 当一个 CPU 修改了 Cache 中的数据,会通知其他缓存了这个数据的 CPU,其他 CPU 会把 Cache 中这份数据的 Cache Line 置为无效,要读取数据的话,直接去内存中获取,不会再从 Cache 中获取了。 当然还有其他的解决方案,MESI 协议是其中比较出名的。 内存模型

MESI(Modified Exclusive Share Invalid)(也称伊利诺斯协议)是一种广泛使用的支持写回策略的缓存一致性协议,该协议被应用在 Intel 奔腾系列的 CPU 中。

MESI 协议中的状态

CPU 中每个缓存行使用的 4 种状态进行标记(使用额外的两位 bit 表示)

状态 描述
M(Modified) 这行数据有效,数据被修改了,和内存中的数据不一样,数据只存在于本 cache 中
E(Exclusive) 这行数据有效,数据和内存中的数据一致,数据只存下于本 Cache 中
S(Shared) 这行数据有效,数据和内存中的数据一致,数据存在于很多 cache 中
I(Invalid) 这行数据无效
  • M 和 E 的数据都是本 core 独有的,不同之处是 M 状态的数据是 dirty(和内存中的不一致),E 状态的数据是 clean(和内存中的一致)
  • S 状态是所有 Core 的数据都是共享的,只有 clean 的数据才能被多个 core 共享
  • I-表示这个 Cache line 无效

E 状态 E状态

只有 Core 0 访问变量 x,它的 Cache line 状态为 E(Exclusive)。

S 状态 S状态

3 个 Core 都访问变量 x,它们对应的 Cache line 为 S(Shared)状态。

M 状态和<I>状态之间的转化 M状态

Core 0 修改了 x 的值之后,这个 Cache line 变成了 M(Modified)状态,其他 Core 对应的 Cache line 变成了 I(Invalid)状态 在 MESI 协议中,每个 Cache 的 Cache 控制器不仅知道自己的读写操作,而且也监听(snoop)其它 Cache 的读写操作。每个 Cache line 所处的状态根据本核和其它核的读写操作在 4 个状态间进行迁移

MESI 协议通过标识缓存数据的状态,来决定 CPU 何时把缓存的数据写入到内存,何时从缓存读取数据,何时从内存读取数据。

3.处理器优化问题

3.1 处理器优化问题

MESI 协议看似解决了缓存的一致性问题,但是并不那么完美,因为当多个缓存对数据进行了缓存时,一个缓存对数据进行修改需要同过指令的形式与其他 CPU 进行通讯,这个过程是同步的,必须其他 CPU 都把缓存里的数据都置为 Invalid 状态成功后,我们修改数据的 CPU 才能进行下一步指令,整个过程中需要同步的和多个缓存通讯,这个过程是不稳定的,容易产生问题,而且通讯的过程中 CPU 是必须处于等待的状态,那么也影响着 CPU 的性能。

3.2 处理器优化解决方案

为了避免这种 CPU 运算能力的浪费,解决 CPU 切换状态阻塞,Store Bufferes 被引入使用。 处理器把它想要写入到主存的值写到缓存,然后继续去处理其他事情。当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。

4.指令重排问题

处理器优化会导致指令重排序问题

value = 3;
void exeToCPUA(){
	value = 10;
	isFinsh = true;
}
void exeToCPUB(){
	if(isFinsh){
		//value一定等于10?!
		assert value == 10;
	}
}

试想一下开始执行时,CPU A 保存着 finished 在 E(独享)状态,而 value 并没有保存在它的缓存中。 (例如,Invalid)。在这种情况下,value 会比 finished 更迟地抛弃存储缓存。 完全有可能 CPU B 读取 finished 的值为 true,而 value 的值不等于 10。即 isFinsh 的赋值在 value 赋值之前。 这种在可识别的行为中发生的变化称为重排序(reordings)。注意,这不意味着你的指令的位置被恶意(或者好意)地更改,它只是意味着其他的 CPU 会读到跟程序中写入的顺序不一样的结果。

指令重排序解决方案: 硬件工程师其无法预知未知的程序逻辑场景,所以一些问题还是遗留给了软件工程师,但是他们给我们提供了一套对应场景的解决方案就是“内存屏障指令”,我们的软件工程师可以同内存屏障来针对不同场景来选择性的“禁用缓存”。

内存屏障分为下面几种:

  1. lfence(读屏障 load Barrier):在读取指令前插入读屏障,让缓存中的数据失效,重新从主内存加载数据,保证数据是最新的。
  2. Sfence(写屏障 store Barrier): 在写入指令后插入屏障,同步把缓存的数据写回内存,保证其数据立即对其他缓存可见。
  3. Mfence(全能屏障):拥有读屏障和写屏障的功能;
  4. Lock 前缀指令:拥有类似全能屏障的功能。
void executedOnCpu0() {
    value = 10;
    //在更新数据之前必须将所有存储缓存(store buffer)中的指令执行完毕。
    storeMemoryBarrier();
    finished = true;
}
void executedOnCpu1() {
    while(!finished);
    //在读取之前将所有失效队列中关于该数据的指令执行完毕。
    loadMemoryBarrier();
    assert value == 10;
}

5.总结:

  • 随着计算机高速发展,CPU 技术远超过内存技术,所以多级缓存被使用,解决了内存和 cpu 的读写速度问题,随着多线程的发展,缓存一致性问题油然而生,好在可以通过缓存一致性协议来解决,比较出名的缓存一致性协议是MESI,MESI协议的引入,微微降低了 cpu 的速度。

  • 为了更好的压榨 cpu 的性能,于是Store Bufferes 概念被引入,将 cpu 写入主存从同步阻塞变为异步,大大提高了 cpu 执行效率

  • 喜闻乐见,指令重排序问题预期而至,这时候祭出终极武器:内存屏障指令,在代码里面禁用缓存。

  • 至此,计算机发展中遇到的问题都一一解决,而这一系列问题解决方案,都是内存模型规范的。

  • 内存模型就是为了解决计算机发展中遇到的缓存一致性、处理器优化和指令重排、并发编程等问题的一系列规范,他定义了共享内存系统中多线程程序读写操作行为的规范,通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。

正文到此结束