多线程之 Cache Line 与伪共享

Posted sunfishgao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程之 Cache Line 与伪共享相关的知识,希望对你有一定的参考价值。

Cache 简介
Cache,即缓存。缓存能提升读取性能,其原理是用性能更好的存储介质存储一部分高频访问的内容,获得总体概率上的速度提升。
在开发中,我们口中的缓存可以是一个变量,或者是 redis。在计算机 CPU 内部,往往指的是 CPU 的各级 cache。
 
Cache 的一致性
由于是高频访问的内容被重复存储到了好几处地方,必然要考虑一致性。你需要及时清除或者更新缓存中过期内容。在程序设计中,使用缓存的架构通常是给定一个过期时间。而对于 CPU Cache,情况就复杂很多。
 
 
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” 共享缓存技术。
CPU Cache 也需要考虑一致性问题,在变量被赋值后, Cache 中的数据就脏了,会被清除。
在多线程环境下,不同 Core 中的 Cache 脏数据会更频繁产生,擦除脏数据的成本开销就会显得很大。
另外值得一提的是,多线程不加锁有可能造成 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 有了脏数据,也是以它为单位整块更新。
 
 

MESI(Modified Exclusive Shared Or Invalid)

摘自 https://www.cnblogs.com/shangxiaofei/p/5688296.html
(也称为伊利诺斯协议,是因为该协议由伊利诺斯州立大学提出)是一种广泛使用的支持写回策略的缓存一致性协议,该协议被应用在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修改了该缓存行)。

 
False Sharing 伪共享
 
伪共享即 MESI 中不健康的 Shared 状态。考虑这样一个场景。
 
struct {
    int thread1_data;
    int thread2_data;
};
 
同时有两个线程(thread1 和 thread2)只去读写属于他自己的那个变量。看似各玩各的互不影响,实际上由于两个变量挨得很近,往往会被放到一个 Cache Line 中。 thread1 对 thread1_data 的读写,会造成 core2 核上对 thread2_data 的缓存被标记为无效 Invalid。我们知道清理 Invalid 状态是很费时的,如果过高频繁地触发,会造成性能下降。
 
在多线程读写数组上,尤其要注意这个伪共享问题。
 
伪共享的本质是,高等语言的概念上,看似变量间是独立的,但是在 CPU Cache 层面, 两个变量地址挨得太近(在一个 Cache Line 范围中)就只能作为整体来看。
 

以上是关于多线程之 Cache Line 与伪共享的主要内容,如果未能解决你的问题,请参考以下文章

高并发多线程基础之线程间通信与数据共享及其应用

3.多线程学习笔记之共享模型之管程

iOS 多线程之线程安全

Java多线程之线程同步

多线程编程之读写锁

多线程资源共享之同步锁