cup伪共享引发的性能影响

今天在学习并发编程时看到一段很有意思的代码,代码如下:

/** 队列中的头部节点 */
private transient f?inal PaddedAtomicReference<QNode> head;
/** 队列中的尾部节点 */
private transient f?inal PaddedAtomicReference<QNode> tail;
static f?inal class PaddedAtomicReference <T> extends AtomicReference T> {
 // 使用很多4个字节的引用追加到64个字节
     Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
     PaddedAtomicReference(T r) {
        super(r);
     }
}
public class AtomicReference <V> implements java.io.Serializable {
     private volatile V value;
     // 省略其他代码
}

这串代码是由著名的Java并发编程大师Doug lea在JDK 7的并发包里新增一个队列集合类LinkedTransferQueue,它在使用volatile变量时,用一种追加字节的方式来优化队列出队和入队的性能。

 

当然,以上都不是今天学习的重点,今天的重点是cup伪共享!

-------------------------------------一下内容均为摘录-----------------------------------------------

认识CPU Cache

CPU Cache概述

 

随着CPU的频率不断提升,而内存的访问速度却没有质的突破,为了弥补访问内存的速度慢,充分发挥CPU的计算资源,提高CPU整体吞吐量,在CPU与内存之间引入了一级Cache。随着热点数据体积越来越大,一级Cache L1已经不满足发展的要求,引入了二级Cache L2,三级Cache L3。(注:若无特别说明,本文的Cache指CPU Cache,高速缓存)CPU Cache在存储器层次结构中的示意如下图:

                                             

 

计算机早已进入多核时代,软件也越来越多的支持多核运行。一个处理器对应一个物理插槽,多处理器间通过QPI总线相连。一个处理器包含多个核,一个处理器间的多核共享L3 Cache。一个核包含寄存器、L1 Cache、L2 Cache,下图是Intel Sandy Bridge CPU架构,一个典型的NUMA多处理器结构:

                                  

 

我们常见的X86芯片:整个Cache被分为S个组,每个组是又由E行个最小的存储单元——Cache Line所组成,而一个Cache Line中有B(B=64)个字节用来存储数据,即每个Cache Line能存储64个字节的数据,每个Cache Line又额外包含一个有效位(valid bit)、t个标记位(tag bit),其中valid bit用来表示该缓存行是否有效;tag bit用来协助寻址,唯一标识存储在CacheLine中的块;而Cache Line里的64个字节其实是对应内存地址中的数据拷贝。根据Cache的结构题,我们可以推算出每一级Cache的大小为B×E×S。

-------------------------------------一上内容均为摘录-----------------------------------------------

Cache Line伪共享及解决方案

Cache Line伪共享分析

说伪共享前,先看看Cache Line 在java编程中使用的场景。如果CPU访问的内存数据不在Cache中(一级、二级、三级),这就产生了Cache Line miss问题,这是,cup又会发送新的指令,从内存中加载数据,越是存储层次靠后的数据,越是消耗性能。故,为提高性能,我们不得不提高缓存的命中率:

 

在java虚拟机规范中,局部变量区等价于一个数组,并且可以用正整数来索引。除了long,double值需要两个数组单元来存储之外,其他基本数据类型均用一个数组单元;boolean,byte,char,short这四种类型,在栈上占用的空间和int一样,和引用类型也是一样。因此,在32位hotspot中,这些类型再栈上占用4个字节;而在64位hotspot中,他们占用8个字节

                                                                                                           --摘录自《极客时间》--《深入理解java虚拟机》

 

看个例子:内存地址是连续的数组(利用空间局部性),能一次被L1缓存加载完成。

 

长度为16的row和column数组,在Cache Line 64字节数据块上内存地址是连续的,能被一次加载到Cache Line中,所以在访问数组时,Cache Line命中率高,性能发挥到极致。

public int run(int[] row, int[] column) {
    int sum = 0;
    for(int i = 0; i < 16; i++ ) {
        sum += row[i] * column[i];
    }
    return sum;
}

而上面例子中变量i则体现了时间局部性,i作为计数器被频繁操作,一直存放在寄存器中,每次从寄存器访问,而不是从主存甚至磁盘访问。虽然连续紧凑的内存分配带来高性能,但并不代表它一直都能带来高性能。如果把它放在多线程中将会发生什么呢?如图:

                                          

 

数据X、Y、Z被加载到同一Cache Line中,线程A在Core1修改X,线程B在Core2上修改Y。根据MESI大法,假设是Core1是第一个发起操作的CPU核,Core1上的L1 Cache Line由S(共享)状态变成M(修改,脏数据)状态,然后告知其他的CPU核,图例则是Core2,引用同一地址的Cache Line已经无效了;当Core2发起写操作时,首先导致Core1将X写回主存,Cache Line状态由M变为I(无效),而后才是Core2从主存重新读取该地址内容,Cache Line状态由I变成E(独占),最后进行修改Y操作, Cache Line从E变成M。可见多个线程操作在同一Cache Line上的不同数据,相互竞争同一Cache Line,导致线程彼此牵制影响,变成了串行程序,降低了并发性。此时我们则需要将共享在多线程间的数据进行隔离,使他们不在同一个Cache Line上,从而提升多线程的性能。

 

故,共享数据,本来是为了提高程序性能,因为缓存行的原因,导致线程彼此牵制影响,变成了串行程序,降低了并发性,这就是伪共享(本人拙见)

 

Cache Line伪共享处理方案

处理伪共享的两种方式:

  1. 增大数组元素的间隔使得不同线程存取的元素位于不同的cache line上。典型的空间换时间。(Linux cache机制与之相关)
  2. 在每个线程中创建全局数组各个元素的本地拷贝,然后结束后再写回全局数组。
public class Data{
    long modifyTime; //修改时间
    boolean flag;    //标记
    long createTime; //创建时间
    char key;        //唯一标识
    int value;       //值
}

上面这个对象中,很明显

  1. 当value变量改变时,modifyTime肯定会改变
  2. createTime变量和key变量在创建后,就不会再变化。
  3. flag也经常会变化,不过与modifyTime和value变量毫无关联。

 

当上面的对象需要由多个线程同时的访问时,从Cache角度来说,就会有一些有趣的问题。当我们没有加任何措施时,Data对象所有的变量极有可能被加载在L1缓存的一行Cache Line中。在高并发访问下,会出现这种问题:

CORE1

CORE2CORE3CORE4

value

value

value

value

modifyTime

modifyTime

modifyTime

modifyTime

flag

flag

flag

flag

createTimecreateTimecreateTimecreateTime

key

key

key

key

         高并发下,不做任何措施,全部存放            ||        只是变更了一个value和modifyTime

                 在一个CacheLine中                         ||

                                                                         V

CORE1

CORE2CORE3CORE4

value

INVALID(失效)

modifyTime

flag

createTime

key

哦豁,之后获取的所有属性,都需要从内存中重新拉取数据了,性能大大的降低了

看到这里,应该就能够想起,为什么我要在片头举得那个例子了吧,对,他就是决绝伪共享的方案1

 

Padding 方式

public class DataPadding{
    long a1,a2,a3,a4,a5,a6,a7,a8;//防止与前一个对象产生伪共享
    int value;
    long modifyTime;
    long b1,b2,b3,b4,b5,b6,b7,b8;//防止不相关变量伪共享;
    boolean flag;
    long c1,c2,c3,c4,c5,c6,c7,c8;//
    long createTime;
    char key;
    long d1,d2,d3,d4,d5,d6,d7,d8;//防止与下一个对象产生伪共享
}

在装载对象的时候,在需要进行数据隔离的数据之前,强行用一组无效的对象进行分割,这样的话,每组数据就不会处于同一cacheLine中了

Contended注解方式

在JDK1.8中,新增了一种注解@sun.misc.Contended,来使各个变量在Cache line中分隔开。注意,jvm需要添加参数-XX:-RestrictContended才能开启此功能 

用时,可以在类前或属性前加上此注释:

// 类前加上代表整个类的每个变量都会在单独的cache line中
@sun.misc.Contended
@SuppressWarnings("restriction")
public class ContendedData {
    int value;
    long modifyTime;
    boolean flag;
    long createTime;
    char key;
}
或者这种:
// 属性前加上时需要加上组标签
@SuppressWarnings("restriction")
public class ContendedGroupData {
    @sun.misc.Contended("group1")
    int value;
    @sun.misc.Contended("group1")
    long modifyTime;
    @sun.misc.Contended("group2")
    boolean flag;
    @sun.misc.Contended("group3")
    long createTime;
    @sun.misc.Contended("group3")
    char key;
}

就,应该无需多解释了吧

 

CORE1

CORE2CORE3CORE4

value

value

value

value

modifyTime

modifyTime

modifyTime

modifyTime

flag

flag

flag

flag

createTimecreateTimecreateTimecreateTime

key

key

key

key

         高并发下,padding,@Contended两种       ||

          方式将会分为三个CacheLine存放                ||

                                                                               V

CORE1

CORE2CORE3CORE4

value

INVALID(失效)

modifyTime

flag

flag

flag

flag

createTimecreateTimecreateTimecreateTime

key

key

key

key

 

 

over!

 

参考资料:

1. 7个示例科普CPU Cache:http://coolshell.cn/articles/10249.html 

2.Linux Cache 机制 :《深入理解计算机系统》http://www.cnblogs.com/liloke/archive/2011/11/20/2255737.html 

3.《极客时间》--《深入理解java虚拟机》--郑雨迪,oracle labs高级远

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值