C++性能榨汁机之伪共享

C++性能榨汁机之伪共享

来源  http://irootlee.com/juicer_false_sharing/

前言

  在多核并发编程中,如果将互斥锁的争用比作“性能杀手”的话,那么伪共享则相当于“性能刺客”。“杀手”与“刺客”的区别在于杀手是可见的,遇到杀手时我们可以选择战斗、逃跑、绕路、求饶等多种手段去应付,但“刺客”却不同,“刺客”永远隐藏在暗处,伺机给你致命一击,防不胜防。具体到我们的并发编程中,遇到锁争用影响并发性能情况时,我们可以采取多种措施(如缩短临界区,原子操作等等)去提高程序性能,但是伪共享却是我们从所写代码中看不出任何蛛丝马迹的,发现不了问题也就无法解决问题,从而导致伪共享在“暗处”严重拖累程序的并发性能,但我们却束手无策。

缓存行

  为了进行下面的讨论,我们需要首先熟悉缓存行的概念,学过操作系统课程存储结构这部分内容的同学应该对存储器层次结构的金字塔模型印象深刻,金字塔从上往下代表存储介质的成本降低、容量变大,从下往上则代表存取速度的提高。位于金字塔模型最上层的是CPU中的寄存器,其次是CPU缓存(L1,L2,L3),再往下是内存,最底层是磁盘,操作系统采用这种存储层次模型主要是为了解决CPU的高速与内存磁盘低速之间的矛盾,CPU将最近使用的数据预先读取到Cache中,下次再访问同样数据的时候,可以直接从速度比较快的CPU缓存中读取,避免从内存或磁盘读取拖慢整体速度。

  CPU缓存的最小单位就是缓存行,缓存行大小依据架构不同有不同大小,最常见的有64Byte和32Byte,CPU缓存从内存取数据时以缓存行为单位进行,每一次都取需要读取数据所在的整个缓存行,即使相邻的数据没有被用到也会被缓存到CPU缓存中(这里又涉及到局部性原理,后面文章会进行介绍)。

缓存一致性

  在单核CPU情况下,上述方法可以正常工作,可以确保缓存到CPU缓存中的数据永远是“干净”的,因为不会有其他CPU去更改内存中的数据,但是在多核CPU下,情况就变得更加复杂一些。多CPU中,每个CPU都有自己的私有缓存(可能共享L3缓存),当一个CPU1对Cache中缓存数据进行操作时,如果CPU2在此之前更改了该数据,则CPU1中的数据就不再是“干净”的,即应该是失效数据,缓存一致性就是为了保证多CPU之间的缓存一致。

  Linux系统中采用MESI协议处理缓存一致性,所谓MESI即是指CPU缓存的四种状态:

  • M(修改,Modified):本地处理器已经修改缓存行,即是脏行,它的内容与内存中的内容不一样,并且此 cache 只有本地一个拷贝(专有);
  • E(专有,Exclusive):缓存行内容和内存中的一样,而且其它处理器都没有这行数据;
  • S(共享,Shared):缓存行内容和内存中的一样, 有可能其它处理器也存在此缓存行的拷贝;
  • I(无效,Invalid):缓存行失效, 不能使用。

  每个CPU缓存行都在四个状态之间互相转换,以此决定CPU缓存是否失效,比如CPU1对一个缓存行执行了写入操作,则此操作会导致其他CPU的该缓存行进入Invalid无效状态,CPU需要使用该缓存行的时候需要从内存中重新读取。由此就解决了多CPU之间的缓存一致性问题。

伪共享

  何谓伪共享?上面我们提过CPU的缓存是以缓存行为单位进行的,即除了本身所需读写的数据之外还会缓存与该数据在同一缓存行的数据,假设缓存行大小是32字节,内存中有“abcdefgh”八个int型数据,当CPU读取“d”这个数据时,CPU会将“abcdefgh”八个int数据组成一个缓存行加入到CPU缓存中。假设计算机有两个CPU:CPU1和CPU2,CPU1只对“a”这个数据进行频繁读写,CPU2只对“b”这个数据进行频繁读写,按理说这两个CPU读写数据没有任何关联,也就不会产生任何竞争,不会有性能问题,但是由于CPU缓存是以缓存行为单位进行存取的,也是以缓存行为单位失效的,即使CPU1只更改了缓存行中“a”数据,也会导致CPU2中该缓存行完全失效,同理,CPU2对“b”的改动也会导致CPU1中该缓存行失效,由此引发了该缓存行在两个CPU之间“乒乓”,缓存行频繁失效,最终导致程序性能下降,这就是伪共享。

如何避免伪共享

  避免伪共享主要有以下两种方式:

  1. 缓存行填充(Padding):为了避免伪共享就需要将可能造成伪共享的多个变量处于不同的缓存行中,可以采用在变量后面填充字节的方式达到该目的。
  2. 使用某些语言或编译器中强制变量对齐,将变量都对齐到缓存行大小,避免伪共享发生。

总结

  一般伪共享都很隐蔽,很难被发现,当伪共享真正构成性能瓶颈的时候,我们有必要去努力找到并解决它,但是在大部分对性能追求没有那么高的应用中,伪共享的存在对程序的危害很小,有时并不值得耗费精力和额外的内存空间(缓存行填充)去查找系统存在的伪共享。还是那句我一直以来遵循的话“不要过度优化,不要提前优化。”。

==================== End

原文地址:https://www.cnblogs.com/lsgxeva/p/11076283.html

时间: 2024-10-30 17:39:41

C++性能榨汁机之伪共享的相关文章

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

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

多线程中的volatile和伪共享

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

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

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

杂谈 什么是伪共享(false sharing)?

问题 (1)什么是 CPU 缓存行? (2)什么是内存屏障? (3)什么是伪共享? (4)如何避免伪共享? CPU缓存架构 CPU 是计算机的心脏,所有运算和程序最终都要由它来执行. 主内存(RAM)是数据存放的地方,CPU 和主内存之间有好几级缓存,因为即使直接访问主内存也是非常慢的. 如果对一块数据做相同的运算多次,那么在执行运算的时候把它加载到离 CPU 很近的地方就有意义了,比如一个循环计数,你不想每次循环都跑到主内存去取这个数据来增长它吧. 越靠近 CPU 的缓存越快也越小. 所以 L

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 缓存中可以分配的最小存储单位.处理器填写缓存线时会加载整个缓存线,

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

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

java 伪共享

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

并发不得不说的伪共享

前言 可谓是一入并发深似海,看得越多,发现自己懂的越少,总感觉自己只是了解了其冰山一角.但是在研究的过程中越来越感受到一些框架的设计之美,很细腻的赶脚.同时也让我get到了新的知识点. CPU缓存 在正式进入正题之前,必须得先说说缓存这个概念.对于缓存这个概念相信大多数程序猿都不会很陌生,在大大小小项目中都会遇到.举个最简单的例子:数据一般都会存放到数据库之中.但在某些应用场景中不可能每次加载数据都去从数据库中加载(毕竟io操作是非常耗时和耗性能的),而是会用redis之类的缓存中间件去过渡,在

多线程之 Cache Line 与伪共享

Cache 简介 Cache,即缓存.缓存能提升读取性能,其原理是用性能更好的存储介质存储一部分高频访问的内容,获得总体概率上的速度提升. 在开发中,我们口中的缓存可以是一个变量,或者是 redis.在计算机 CPU 内部,往往指的是 CPU 的各级 cache. Cache 的一致性 由于是高频访问的内容被重复存储到了好几处地方,必然要考虑一致性.你需要及时清除或者更新缓存中过期内容.在程序设计中,使用缓存的架构通常是给定一个过期时间.而对于 CPU Cache,情况就复杂很多. CPU Ca