【欧冠体育体育竞猜 官网入口】

字节面:什么是伪同享?

发布日期:2022-08-07 07:55    点击次数:167

 

本文转载自微信群众号「小林coding」,作者小林coding 。转载本文请联络小林coding群众号。

巨匠好,我是小林。

周末的时光,有个读者跟我说,笔试字节的时光被问到:「什么是伪同享?又该怎么防止伪同享的成就?」

这个着实是审核 CPU 缓存的成就,我从前的图瓦解系也有提到过。

来日诰日,我再跟巨匠讲一下。

正文 CPU 怎么读写数据的?

先来熟习 CPU 的架构,只要懂患有 CPU 的 架构,材干更好天文解 CPU 是怎么读写数据的,关于今世 CPU 的架构图以下:

可以或许看到,一个 CPU 里平日会有多个 CPU 焦点,比喻上图中的 1 号和 2 号 CPU 焦点,并且每个 CPU 焦点都有自身的 L1 Cache 和 L2 Cache,而 L1 Cache 平日分为 dCache(数据缓存) 和 iCache(指令缓存),L3 Cache 则是多个焦点同享的,这就是 CPU 典范的缓存条理。

上面提到的都是 CPU 外部的 Cache,放眼外部的话,还会有内存和硬盘,这些存储动作举措怪异造成为了金字塔存储条理。以下图所示:

从上图也可以看到,从上往下,存储动作举措的容量会越大,而拜访速度会越慢。至于每个存储动作举措的拜访延时,你可以或许看下图的表格:

你可以或许看到, CPU 拜访 L1 Cache 速度比拜访内存快 100 倍,这就是为何 CPU 里会有 L1~L3 Cache 的启事,目标就是把 Cache 作为 CPU 与内存之间的缓存层,以削减对内存的拜访频次。

CPU 从内存中读取数据到 Cache 的时光,着实不是一个字节一个字节读取,而是一块一块的编制来读取数据的,这一块一块的数据被称为 CPU Line(缓存行),所以 CPU Line 是 CPU 从内存读取数据到 Cache 的单位。

至于 CPU Line 大小,在 Linux 体系可以或许用上面的编制查察到,你可以或许看我服务器的 L1 Cache Line 大小是 64 字节,也就意味着 L1 Cache 一次载入数据的大小是 64 字节。

那末对数组的加载, CPU 就会加载数组内里间断的多个数据到 Cache 里,是以我们该当根据物理内存地点漫衍的按次去拜访元素,这样拜访数组元素的时光,Cache 射中率就会很高,是以便可以或许削减从内存读取数据的频次, 从而可行进顺序的性能。

然则,在我们不应用数组,而是应用零丁的变量的时光,则会有 Cache 伪同享的成就,Cache 伪同享成就上是一共性能杀手,我们该当要规避它。

接上去,就来看看 Cache 伪同享是什么?又怎么防止这个成就?

今朝假设有一个双焦点的 CPU,这两个 CPU 焦点并行运行着两个差别的线程,它们同时从内存中读取两个差别的数据,划分是范例为 long 的变量 A 和 B,这个两个数据的地点在物理内存上是间断的,假设 Cahce Line 的大小是 64 字节,并且变量 A 在 Cahce Line 的结尾职位地方,那末这两个数据是位于同一个 Cache Line 中,又因为 CPU Line 是 CPU 从内存读取数据到 Cache 的单位,所以这两个数据会被同时读入到了两个 CPU 焦点中各自 Cache 中。

我们来思虑一个成就,假设这两个差别焦点的线程划分编削差别的数据,比喻 1 号 CPU 焦点的线程只编削了 变量 A,或 2 号 CPU 焦点的线程的线程只编削了变量 B,会发生什么呢?

阐发伪同享的成就

今朝我们联结担保多核缓存分歧的 MESI 和谈,来批注这一全副的进程,假设你还不晓得 MESI 和谈,你可以或许看我这篇文章「10 张图关上 CPU 缓存分歧性的大门」。

①. 最起头变量 A 和 B 都还不在 Cache 内里,假设 1 号焦点绑定了线程 A,2 号焦点绑定了线程 B,线程 A 只会读写变量 A,线程 B 只会读写变量 B。

②. 1 号焦点读取变量 A,因为 CPU 从内存读取数据到 Cache 的单位是 Cache Line,也恰好变量 A 和 变量 B 的数据归属于同一个 Cache Line,所以 A 和 B 的数据都市被加载到 Cache,并将此 Cache Line 标记为「独占」形态。

③. 接着,2 号焦点起头从内存里读取变量 B,一样的也是读取 Cache Line 大小的数据到 Cache 中,此 Cache Line 中的数据也包孕了变量 A 和 变量 B,通知公告此时 1 号和 2 号焦点的 Cache Line 形态变为「同享」形态。

④. 1 号焦点需求编削变量 A,缔造此 Cache Line 的形态是「同享」形态,所以先需求经由过程总线发送音讯给 2 号焦点,看护 2 号焦点把 Cache 中对应的 Cache Line 标记为「已失效」形态,尔后 1 号焦点对应的 Cache Line 形态变成「已编削」形态,并且编削变量 A。

⑤. 当前,2 号焦点需求编削变量 B,此时 2 号焦点的 Cache 中对应的 Cache Line 是已失效形态,此外因为 1 号焦点的 Cache 也有此沟通的数据,且形态为「已编削」形态,所以要先把 1 号焦点的 Cache 对应的 Cache Line 写回到内存,尔后 2 号焦点再从内存读取 Cache Line 大小的数据到 Cache 中,最后把变量 B 编削到 2 号焦点的 Cache 中,并将形态标记为「已编削」形态。

所以,可以或许缔造假设 1 号和 2 号 CPU 焦点这样继续交替的划分编削变量 A 和 B,就会重复 ④ 和 ⑤ 这两个步伐,Cache 并无起到缓存的结果,诚然变量 A 和 B 之间着实并无任何的纠葛,然则因为同时归属于一个 Cache Line ,这个 Cache Line 中的肆意数据被编削后,都市互相影响,从而出现 ④ 和 ⑤ 这两个步伐。

是以,这类因为多个线程同时读写同一个 Cache Line 的差别变量时,而导致 CPU Cache 失效的景象称为伪同享(False Sharing)。

防止伪同享的编制

是以,关于多个线程同享的热点数据,即常常会编削的数据,该当防止这些数据恰好在同一个 Cache Line 中,否则就会出现为伪同享的成就。

接上去,看看无理论名目中是用什么编制来防止伪同享的成就的。

在 Linux 内核中存在 __cacheline_aligned_in_smp 宏定义,是用于经管伪同享的成就。

从上面的宏定义,我们可以或许看到:

假设在多核(MP)体系里,该宏定义是 __cacheline_aligned,也就是 Cache Line 的大小; 而假设在单核体系里,该宏定义是空的;

是以,针对在同一个 Cache Line 中的同享的数据,假设在多核之间竞争相比重大,为了预防伪同享景象的发生,可以或许给与上面的宏定义使得变量在 Cache Line 里是对齐的。

举个例子,有上面这个组织体:

组织体里的两个成员变量 a 和 b 在物理内存地点上是间断的,是以它们可以或许会位于同一个 Cache Line 中,以下图:

所以,为了预防前面提到的 Cache 伪同享成就,我们可应用上面介绍的宏定义,将 b 的地点配置为 Cache Line 对齐地点,以下:

这样 a 和 b 变量就不会在同一个 Cache Line 中了,以下图:

所以,防止 Cache 伪同享着实是用空间换时光的思想,糟践一部份 Cache 空间,从而换来性能的提升。

我们再来看一个应用层面的规避计划,有一个 Java 并发框架 Disruptor 应用「字节填充 + 继承」的编制,来防止伪同享的成就。

Disruptor 中有一个 RingBuffer 类会常常被多个线程应用,代码以下:

你可以或许会感应 RingBufferPad 类里 7 个 long 范例的名字很稀罕,但现实上,它们诚然看起来毫无浸染,但却对性能的提升起到了至关首要的浸染。

我们都晓得,CPU Cache 从内存读取数据的单位是 CPU Line,普通 64 位 CPU 的 CPU Line 的大小是 64 个字节,一个 long 范例的数据是 8 个字节,所以 CPU 一下会加载 8 个 long 范例的数据。

痛处 JVM 工具继承纠葛中父类成员和子类成员,内存地点是间断陈设计划的,是以 RingBufferPad 中的 7 个 long 范例数据作为 Cache Line 前置填充,而 RingBuffer 中的 7 个 long 范例数据则作为 Cache Line 后置填充,这 14 个 long 变量没有任何理论用途,更不会对它们举行读写操作。

此外,RingBufferFelds 内里定义的这些变量都是 final 修饰的,意味着第一次加载当前不会再编削, 又因为「先后」各填充了 7 个不会被读写的 long 范例变量,所以不管怎么加载 Cache Line,这全副 Cache Line 里都没有会发生更新操作的数据,是以只需数据被频繁地读取拜访,就自然没有数据被换出 Cache 的可以或许,也是以不会孕育发生伪同享的成就。