转载自:http://ifeve.com/from-javaeye-cpu-cache/
              http://ifeve.com/from-javaeye-false-sharing/

CPU是计算机的大脑,它负责执行程序的指令;内存负责存数据,包括程序自身数据。内存比CPU慢很多,现在获取内存中的一条数据大概需要200多个CPU周期(CPU cycles),而CPU寄存器一般情况下1个CPU周期就够了。
     
 网页浏览器为了加快速度,会在本机存缓存以前浏览过的数据;传统数据库或NoSQL数据库为了加速查询,常在内存设置一个缓存,减少对磁盘(慢)的IO。同样内存与CPU的速度相差太远,于是CPU设计者们就给CPU加上了缓存(CPU

Cache)。如果需要对同一批数据操作很多次,那么把数据放至离CPU更近的缓存,会给程序带来很大的速度提升。例如,做一个循环计数,把计数变量放到缓存里,就不用每次循环都往内存存取数据了。下面是CPU
Cache的简单示意图:
       
       随着多核的发展,CPU Cache分成了三个级别:L1、
L2、L3。级别越小越接近CPU,所以速度也更快,同时也代表着容量越小。L1是最接近CPU的,它容量最小,例如32K,速度最快,每个核上都有一个L1
Cache(准确地说每个核上有两个L1 Cache,一个存数据 L1d Cache,一个存指令 L1i Cache)。L2 Cache
更大一些,例如256K,速度要慢一些,一般情况下每个核上都有一个独立的L2 Cache;L3
Cache是三级缓存中最大的一级,例如12MB,同时也是最慢的一级,在同一个CPU插槽之间的核共享一个L3
Cache。
       就像数据库cache一样,获取数据时首先会在最快的cache中找数据,如果没有命中(Cache miss)则往下一级找,直到三层Cache都找不到,那只有向内存要数据了。一次次地未命中,代表取数据消耗的时间越长。
     
 为了高效地存取缓存,不是简单随意地将单条数据写入缓存的。缓存是由缓存行组成的,典型的一行是64字节。CPU存取缓存都是按行为最小单位操作的。一个Java

long型占8字节,所以从一条缓存行上可以获取到8个long型变量。所以如果访问一个long型数组,当有一个long被加载到cache中,将会无消耗地加载了另外7个,所以可以非常快地遍历数组。

既然典型的CPU微架构有3级缓存,每个核都有自己私有的L1、 L2缓存,那么多线程编程时,另外一个核的线程想要访问当前核内L1、L2缓存行的数据时,该怎么办呢?
       有一种办法可以通过第2个核直接访问第1个核的缓存行。这是可行的,但这种方法不够快。跨核访问需要通过Memory
Controller,典型的情况是第2个核经常访问第1个核的这条数据,那么每次都有跨核的消耗。更糟的情况是,有可能第2个核与第1个核不在一个插槽内,况且Memory

Controller的总线带宽是有限的,扛不住这么多数据传输。所以CPU设计者们更偏向于另一种办法:如果第2个核需要这份数据,由第1个核直接把数据内容发过去,数据只需要传一次。
       那么什么时候会发生缓存行的传输呢?答案很简单:当一个核需要读取另外一个核的脏缓存行时发生。但是前者怎么判断后者的缓存行已经被弄脏(写)了呢?
       下面将详细地解答以上问题. 首先需要谈到一个协议---MESI协议。现在主流的处理器都是用它来保证缓存的相干性和内存的相干性。M、E、S和I代表使用MESI协议时缓存行所处的四个状态:
       M(修改,Modified):本地处理器已经修改缓存行, 即是脏行, 它的内容与内存中的内容不一样. 并且此cache只有本地一个拷贝(专有).
       E(专有,Exclusive):缓存行内容和内存中的一样, 而且其它处理器都没有这行数据.
       S(共享,Shared):缓存行内容和内存中的一样, 有可能其它处理器也存在此缓存行的拷贝.
       I(无效,Invalid):缓存行失效, 不能使用.

下面简单地说明下缓存行的四种状态怎么转换的:
        初始:一开始时,缓存行没有加载任何数据,所以它处于I状态。
        本地写(Local Write):如果本地处理器写数据至处于I状态的缓存行,则缓存行的状态变成M。
        本地读(Local Read):如果本地处理器读取处于I状态的缓存行,
很明显此缓存没有数据给它。此时分两种情况:(1)其它处理器的缓存里也没有此行数据,则从内存加载数据到此缓存行后,再将它设成E状态,表示只有我一家有这条数据,其它处理器都没有;(2)其它处理器的缓存有此行数据,则将此缓存行的状态设为S状态。P.S.如果处于M状态的缓存行,再由本地处理器写入/读出,状态是不会改变的。
        远程读(Remote Read):假设有两个处理器c1和c2。如果c2需要读另外一个处理器c1的缓存行内容,c1需要把它缓存行的内容通过内存控制器(Memory Controller)发送给c2,c2接到后将相应的缓存行状态设为S。在设置之前,内存也得从总线上得到这份数据并保存。
        远程写(Remote Write):其实确切地说不是远程写,而是c2得到c1的数据后,不是为了读,而是为了写,也算是本地写,只是c1也拥有这份数据的拷贝,这该怎么办呢?c2将发出一个RFO(Request
For
Owner)请求,它需要拥有这行数据的权限,其它处理器的相应缓存行设为I,除了它自已,谁不能动这行数据。这保证了数据的安全,同时处理RFO请求以及设置I的过程将给写操作带来很大的性能消耗。

上述内容知道,写操作的代价很高,特别当需要发送RFO消息时。那编写程序时,什么时候会发生RFO请求呢?有以下两种:
        1. 线程的工作从一个处理器移到另一个处理器,它操作的所有缓存行都需要移到新的处理器上。此后如果再写缓存行,则此缓存行在不同核上有多个拷贝,需要发送RFO请求了。
        2. 两个不同的处理器确实都需要操作相同的缓存行。

在Java程序中,数组的成员在缓存中也是连续的。其实从Java对象的相邻成员变量也会加载到同一缓存行中。如果多个线程操作不同的成员变量,但是相同的缓存行,伪共享(False Sharing)问题就发生了。
       举个例子:一个运行在处理器core 1上的线程想要更新变量X的值,同时另外一个运行在处理器core
2上的线程想要更新变量Y的值。但是,这两个频繁改动的变量都处于同一条缓存行。两个线程就会轮番发送RFO消息,占得此缓存行的拥有权。当core
1取得了拥有权开始更新X,则core 2对应的缓存行需要设为I状态。当core 2取得了拥有权开始更新Y,则core
1对应的缓存行需要设为I状态(失效态)。轮番夺取拥有权不但带来大量的RFO消息,而且如果某个线程需要读此行数据时,L1和L2缓存上都是失效数据,只有L3缓存上是同步好的数据。读L3的数据非常影响性能,更坏的情况是跨槽读取,L3都要miss,只能从内存上加载。
       表面上X和Y都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。

那么怎么避免伪共享呢?一条缓存行有64字节,而Java程序的对象头固定占8字节(32位系统)或12字节(64位系统默认开启压缩,
不开压缩为16字节)。只需要填6个无用的长整型补上6*8=48字节,让不同的变量处于不同的缓存行,就可以避免伪共享了(64位系统超过缓存行的64字节也无所谓,只要保证不同线程不要操作同一缓存行就可以),这个办法叫做补齐(Padding)。例如:

  1. public final static class VolatileLong {
  2. public volatile long value = 0L;
  3. public long p1, p2, p3, p4, p5, p6;
  4. }

伪共享在多核编程中很容易发生,而且比较隐蔽。例如在JDK的LinkedBlockingQueue中,存在指向队列头的引用head和指向队列尾的引用last。而这种队列经常在异步编程中使有,这两个引用的值经常的被不同的线程修改,但它们却很可能在同一个缓存行,于是就产生了伪共享。线程越多,核越多,对性能产生的负面效果就越大。
       某些Java编译器会将没有使用到的补齐数据,即使示例代码中的6个长整型在编译时优化掉,可以在程序中加入一些代码防止被编译优化。

  1. public static long preventFromOptimization(VolatileLong v) {
  2. return v.p1 + v.p2 + v.p3 + v.p4 + v.p5 + v.p6;
  3. }

另外,由于Java的GC问题。数据在内存和对应的CPU缓存行的位置有可能发生变化,所以在使用pad的时候应该注意GC的影响。

从Java视角理解CPU缓存和伪共享的更多相关文章

  1. 从Java视角理解CPU缓存(CPU Cache)

    从Java视角理解系统结构连载, 关注我的微博(链接)了解最新动态众所周知, CPU是计算机的大脑, 它负责执行程序的指令; 内存负责存数据, 包括程序自身数据. 同样大家都知道, 内存比CPU慢很多 ...

  2. 从Java视角理解CPU上下文切换(Context Switch)

    从Java视角理解系统结构连载, 关注我的微博(链接)了解最新动态   在高性能编程时,经常接触到多线程. 起初我们的理解是, 多个线程并行地执行总比单个线程要快, 就像多个人一起干活总比一个人干要快 ...

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

    volatilekeyword 当变量被某个线程A改动值之后.其他线程比方B若读取此变量的话,立马能够看到原来线程A改动后的值 注:普通变量与volatile变量的差别是volatile的特殊规则保证 ...

  4. 【转】七个例子帮你更好地理解 CPU 缓存

    我的大多数读者都知道缓存是一种快速.小型.存储最近已访问的内存的地方.这个描述相当准确,但是深入处理器缓存如何工作的"枯燥"细节,会对尝试理解程序性能有很大帮助. 在这篇博文中,我 ...

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

    关于伪共享的文章已经很多了,对于多线程编程来说,特别是多线程处理列表和数组的时候,要非常注意伪共享的问题.否则不仅无法发挥多线程的优势,还可能比单线程性能还差.随着JAVA版本的更新,再各个版本上减少 ...

  6. 什么是CPU缓存

    一.什么是CPU缓存 1. CPU缓存的来历 众所周知,CPU是计算机的大脑,它负责执行程序的指令,而内存负责存数据, 包括程序自身的数据.在很多年前,CPU的频率与内存总线的频率在同一层面上.内存的 ...

  7. java 伪共享

    MESI协议及RFO请求典型的CPU微架构有3级缓存, 每个核都有自己私有的L1, L2缓存. 那么多线程编程时, 另外一个核的线程想要访问当前核内L1, L2 缓存行的数据, 该怎么办呢?有人说可以 ...

  8. 缓存行和cpu缓存实例

    并发框架Disruptor译文 剖析Disruptor:为什么会这么快?(一)锁的缺点 剖析Disruptor:为什么会这么快?(二)神奇的缓存行填充 剖析Disruptor:为什么会这么快?(三)伪 ...

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

    在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素.前段时间学习了一个牛X的高性能异步处理框架 Disruptor ...

随机推荐

  1. STL+Floyd【p1690】贪婪的Copy

    Description Copy从卢牛那里听说在一片叫yz的神的领域埋藏着不少宝藏,于是Copy来到了这个被划分为个区域的神地.卢牛告诉了Copy这里共有个宝藏,分别放在第Pi个(1<=Pi&l ...

  2. BZOJ 2243 [SDOI2011]染色 (树链剖分)(线段树区间修改)

    [SDOI2011]染色 Time Limit: 20 Sec  Memory Limit: 512 MBSubmit: 6870  Solved: 2546[Submit][Status][Disc ...

  3. 小程序 wx:for 循环嵌套

    json数据: [//library-6F    [//library-6F-601      [//id:1-1 ,8(Y/N),9(Y/N)……21(Y/N)        'Y','Y','Y' ...

  4. hive参数——深入浅出学Hive

    第一部分:Hive 参数 hive.exec.max.created.files •说明:所有hive运行的map与reduce任务可以产生的文件的和 •默认值:100000  hive.exec.d ...

  5. vi中使用“/”查找字符

    在vi 文件中使用"/"查找字符串 命令模式下,输入 /word 后回车,即查找word,按 n 查找下一个匹配单词,按 N 查找上一个匹配单词.

  6. ubuntu16.04 ssh服无法远程连接解决办法

    1.安装ssh服务sudo apt-get install openssh-server 2.修改配置文件sudo vi /etc/ssh/sshd_config#PermitRootLogin wi ...

  7. ES6里关于类的拓展(二):继承与派生类

    继承与派生类 在ES6之前,实现继承与自定义类型是一个不小的工作.严格意义上的继承需要多个步骤实现 function Rectangle(length, width) { this.length = ...

  8. 用于快速排查Java的CPU性能问题(top us值过高)

    转载于GIT路径 https://github.com/oldratlee/useful-scripts/blob/master/docs/java.md#beer-show-busy-java-th ...

  9. 通过脚本发送zabbix邮件报警

    zabbix原生的报警媒介类型中,邮件报警是我们常用的方式.当我们在CentOS6上面安装zabbix3.0并配置邮件报警的时候,在邮件配置正确的前提下,不管触发器如何触发,邮件总是发送不出去,但是在 ...

  10. Java Servlet 技术简介 from:IBM Dev

    点评:讲的没有太听懂. from: http://www.ibm.com/developerworks/cn/education/java/j-intserv/index.html#resources