多线程之 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 范围中)就只能作为整体来看。

原文地址:https://www.cnblogs.com/sunfishgao/p/10393298.html

时间: 2024-08-28 13:14:57

多线程之 Cache Line 与伪共享的相关文章

多线程中的volatile和伪共享

  伪共享 false sharing,顾名思义,“伪共享”就是“其实不是共享”.那什么是“共享”?多CPU同时访问同一块内存区域就是“共享”,就会产生冲突,需要控制协议来协调访问.会引起“共享”的最小内存区域大小就是一个cache line.因此,当两个以上CPU都要访问同一个cache line大小的内存区域时,就会引起冲突,这种情况就叫“共享”.但是,这种情况里面又包含了“其实不是共享”的“伪共享”情况.比如,两个处理器各要访问一个word,这两个word却存在于同一个cache line

cache缓存与伪共享

一.cache缓存 cache与主存之间是以块为单位读写的,这样设计是为了符合程序运行的局部性原理--时间局部性原理与空间局部性原理(参见<计算机组成原理>) 二维数组行遍历比列遍历要快,是由于二维数组是按行存储的,cache从主存中读入块,会将同行相邻元素一起写入cache,导致行遍历cache命中率大于列遍历cache命中率. public class CacheTest { static final int LINE_NUM = 1024; static final int COLUMN

从缓存行出发理解volatile变量、伪共享False sharing、disruptor

volatilekeyword 当变量被某个线程A改动值之后.其他线程比方B若读取此变量的话,立马能够看到原来线程A改动后的值 注:普通变量与volatile变量的差别是volatile的特殊规则保证了新值能马上同步到主内存,以及每次使用前能够马上从内存刷新,即一个线程改动了某个变量的值,其他线程读取的话肯定能看到新的值. 普通变量: 写命中:当处理器将操作数写回到一个内存缓存的区域时.它首先会检查这个缓存的内存地址是否在缓存行中,假设不存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不

伪共享(false sharing),并发编程无声的性能杀手

在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素.前段时间学习了一个牛X的高性能异步处理框架 Disruptor,它被誉为“最快的消息框架”,其 LMAX 架构能够在一个线程里每秒处理 6百万 订单!在讲到 Disruptor 为什么这么快时,接触到了一个概念——伪共享( false sharing ),其中提到:缓存行上的写竞争是运行在 SMP 系统中并行线程实现可伸缩性最重要的限制因素.由于从代码中很难看

伪共享和缓存行填充,从Java 6, Java 7 到Java 8

关于伪共享的文章已经很多了,对于多线程编程来说,特别是多线程处理列表和数组的时候,要非常注意伪共享的问题.否则不仅无法发挥多线程的优势,还可能比单线程性能还差.随着JAVA版本的更新,再各个版本上减少伪共享的做法都有区别,一不小心代码可能就失效了,要注意进行测试.这篇文章总结一下. 什么是伪共享 关于伪共享讲解最清楚的是这篇文章<剖析Disruptor:为什么会这么快?(三)伪共享>,我这里就直接摘抄其对伪共享的解释: 缓存系统中是以缓存行(cache line)为单位存储的.缓存行是2的整数

伪共享 FalseSharing (CacheLine,MESI) 浅析以及Java里的解决方案

起因 在阅读百度的发号器 uid-generator 源码的过程中,发现了一段很奇怪的代码: /** * Represents a padded {@link AtomicLong} to prevent the FalseSharing problem<p> * * The CPU cache line commonly be 64 bytes, here is a sample of cache line after padding:<br> * 64 bytes = 8 by

共享和伪共享

共享就是一个内存区域的数据被多个处理器访问,伪共享就是不是真的共享.这里的共享这个概念是基于逻辑层面的.实际上伪共享与共享在cache line 上实际都是共享的. CPU访问的数据都是从cache line 中读取的.如果cpu 在cache 中找不到需要的变量,则称缓存未命中. 未命中时,需要通过总线从内存中读取进cache 中.每次读取的内存大小就是一个cache line 的大小. 如果多个CPU访问的不同内存变量被装载到了同一个cache line 中,则从程序逻辑层上讲,并没有共享变

Disruptor的伪共享解决方案

1.术语 术语 英文单词 描述 内存屏障 Memory Barriers 是一组处理器指令,用于实现对内存操作的顺序限制. In the Java Memory Model a volatile field has a store barrier inserted after a write to it and a load barrier inserted before a read of it. 缓存行 Cache line 缓存中可以分配的最小存储单位.处理器填写缓存线时会加载整个缓存线,

java 伪共享

MESI协议及RFO请求典型的CPU微架构有3级缓存, 每个核都有自己私有的L1, L2缓存. 那么多线程编程时, 另外一个核的线程想要访问当前核内L1, L2 缓存行的数据, 该怎么办呢?有人说可以通过第2个核直接访问第1个核的缓存行. 这是可行的, 但这种方法不够快. 跨核访问需要通过Memory Controller(见上一篇的示意图), 典型的情况是第2个核经常访问第1个核的这条数据, 那么每次都有跨核的消耗. 更糟的情况是, 有可能第2个核与第1个核不在一个插槽内.况且Memory C