多核编程之 CPU Cache

Cache 简介

Cache,即缓存。缓存能提升读取性能,其原理是用性能更好的存储介质存储一部分高频访问的内容,获得总体概率上的速度提升。

在开发中,我们口中的缓存可以是一个变量,或者是 redis。在计算机 CPU 内部,CPU 往往指的是 CPU 的各级缓存。

CPU Cache 原理

缓存的工作原理是当 CPU 要读取一个数据时,首先从CPU缓存中查找,找到就立即读取并送给 CPU 处理;没有找到,就从速率相对较慢的内存中读取并送给 CPU 处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。正是这样的读取机制使 CPU 读取缓存的命中率非常高(大多数 CPU 可达 90% 左右),也就是说 CPU 下一次要读取的数据 90% 都在 CPU 缓存中,只有大约 10% 需要从内存读取。这大大节省了 CPU 直接读取内存的时间,也使 CPU 读取数据时基本无需等待。总的来说,CPU 读取数据的顺序是先缓存后内存。(摘自百科)

将模型简化以后,如果 CPU 想访问内存里的内容:

CPU Core1  --> L1 Cache --> L2 Cache  --> L3 Cache --> RAM

CPU Core2  --> L1 Cache --> L2 Cache  --> L3 Cache --> RAM

需要注意的是,简单情况下,每个 CPU 核心都有自己的独立的多级缓存,常见的有三级。访问速度上,L1 > L2 > L3, 容量通常与速度成反比。通俗点说,你在某处声明的变量 int foo = 1;在有缓存情况下,CPU 是从 L1~L3 中获取 foo 的值,多级缓存无命中才去内存中取。

现如今 Intel 比较新的 CPU 型号,其缓存不再是彼此独立的设计了,双核会共享二级缓存,即“Smart cache” 共享缓存技术。

Cache Line

将 Cache 按照一定长度切割开,就有了许多的 Cache Line。Cache Line 是 Cache 的最小单位,通常是 64 bytes。如果 L1 缓存是 6400 bytes, 那他可以分成 100 个 Cache Line。在 C 语言中,你能感知到的内存最小单位应该是变量, int,long long 等,他们通常只有 4 字节或者 8 字节。CPU 的缓存为了性能,一般是以 Cache Line 为单位进行一口气缓存一大块内存。一个 Cache Line 中就会缓存很多个变量的值。如果 Cache Line 有了脏数据,也是以它为单位整块更新。

Cache 的一致性

计算机必须保证在缓存中的数据永远都是新的。如果内存值已经发生改变,CPU Cache 未及时同步,就出现了数据不一致。多核 CPU 架构下,一致性的保证就比较复杂,比如多个 CPU Cache 都缓存了某个变量的值,但那个变量被其中一个核修改了值,其他 CPU 核心内的缓存如何能及时感知并刷新缓存?

MESI 协议能解决多核 CPU Cache 一致性问题。

MESI(Modified Exclusive Shared Or Invalid) 摘自 https://www.cnblogs.com/shangxiaofei/p/5688296.html

 MESI 也称为伊利诺斯协议,是因为该协议由伊利诺斯州立大学提出 是一种广泛使用的支持写回策略的缓存一致性协议,该协议被应用在Intel奔腾系列的CPU中。

MESI协议中的状态

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

M: 被修改(Modified)

该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。

当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。

E: 独享的(Exclusive)

该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。

同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。

S: 共享的(Shared)

该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,

其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。

I: 无效的(Invalid)

该缓存是无效的(可能有其它CPU修改了该缓存行)。

 简单总结:

CPU 各个核之间存在一种通讯的方式,用于通知其他核心某个 Cache Line 已经失效了。CPU 对 Cache 的读写操作之前,会判断该 Cache Line 处于哪个状态。

当 Cache 处于 Shared 状态时,CPU 某核心执行写操作,会广播通知到其他 CPU 核心。

通过这种方式,保证了 Cache 的一致性。

Cache Miss

我们知道 CPU 需要 Cache 来提高数据读取速度。如果 CPU 想访问一块内存,Cache 里没有,我们管他叫 Cache Miss。Cache Miss 会让你头大,CPU 不得不花费大量的时间用在把内存数据加载进 Cache。一般来说, L1 的 miss 率在 10% 左右。倒过来想想,居然有 90% 左右的命中率,不得不佩服程序员大神的能力。

False Sharing 伪共享

伪共享即 MESI 中不健康的 Shared/Invalid 状态。考虑这样一个场景。

1

2

3

4

struct {

    int thread1_data;   // 线程1只读写它

    int thread2_data;   // 线程2只读写它

};

  

同时有两个线程(thread1 和 thread2)只去读写属于他自己的那个变量。看似各玩各的互不影响,实际上由于两个变量挨得很近,往往会被放到一个 Cache Line 中。 thread1 对 thread1_data 的读写,会造成 core2 核上对 thread2_data 的缓存被标记为无效 Invalid,从而要刷新 Cache。我们知道将内存装载进 Cache 是很费时的,如果过高频繁地触发,会造成性能下降。

在多线程读写数组上,尤其要注意这个伪共享问题。

 伪共享的本质是,高等语言的概念上,看似变量间是独立的,但是在 CPU Cache 层面, 两个变量地址挨得太近(在一个 Cache Line 范围中)就只能作为一个 Cache Line 整体来看。

CPU 指令乱序与 Barrier 屏障

由于存在 Cache Miss 等耗时工作,但是 CPU 可以在加载数据的同时,干一些别的事情,那么指令必然会被打乱。

总之, CPU 指令不按顺序执行是为了更快的性能,更高的执行效率。

指令乱序就发生在我们身边,举个通俗的例子,

1

2

a = 1;

b = 2;

在 CPU 指令乱序的情况下,a 和 b 谁先被赋值是不知道的。如果多线程依赖了其先后顺序,不加锁的情况下会造成很严重的问题。

barrier 指令可以解决 CPU 指令乱序的问题。它告诉 CPU 某些地方不要乱序。这是个底层的指令,对于高级语言使用者来说,应该使用加锁、原子操作来解决。

发布了74 篇原创文章 · 获赞 337 · 访问量 130万+

猜你喜欢

转载自blog.csdn.net/kebu12345678/article/details/103990909