天天看点

从Java视角理解伪共享(False Sharing)

作者:coderplay

有人说可以通过第2个核直接访问第1个核的缓存行. 这是可行的, 但这种方法不够快. 跨核访问需要通过memory controller(见上一篇的示意图), 典型的情况是第2个核经常访问第1个核的这条数据, 那么每次都有跨核的消耗. 更糟的情况是, 有可能第2个核与第1个核不在一个插槽内.况且memory controller的总线带宽是有限的, 扛不住这么多数据传输. 所以, cpu设计者们更偏向于另一种办法: 如果第2个核需要这份数据, 由第1个核直接把数据内容发过去, 数据只需要传一次.

那么什么时候会发生缓存行的传输呢? 答案很简单: 当一个核需要读取另外一个核的脏缓存行时发生. 但是前者怎么判断后者的缓存行已经被弄脏(写)了呢?

m(修改, modified): 本地处理器已经修改缓存行, 即是脏行, 它的内容与内存中的内容不一样. 并且此cache只有本地一个拷贝(专有).

e(专有, exclusive): 缓存行内容和内存中的一样, 而且其它处理器都没有这行数据

s(共享, shared): 缓存行内容和内存中的一样, 有可能其它处理器也存在此缓存行的拷贝

i(无效, invalid): 缓存行失效, 不能使用

从Java视角理解伪共享(False Sharing)

初始 一开始时, 缓存行没有加载任何数据, 所以它处于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. 两个不同的处理器确实都需要操作相同的缓存行

表面上x和y都是被独立线程操作的, 而且两操作之间也没有任何关系.只不过它们共享了一个缓存行, 但所有竞争冲突都是来源于共享.

实验及分析

引用martin的例子, 稍做修改,代码如下:

java代码

public final class falsesharing implements runnable {  

    public static int num_threads = 4; // change  

    public final static long iterations = 500l * 1000l * 1000l;  

    private final int arrayindex;  

    private static volatilelong[] longs;  

    public falsesharing(final int arrayindex) {  

        this.arrayindex = arrayindex;  

    }  

    public static void main(final string[] args) throws exception {  

        thread.sleep(10000);  

        system.out.println("starting....");  

        if (args.length == 1) {  

            num_threads = integer.parseint(args[0]);  

        }  

        longs = new volatilelong[num_threads];  

        for (int i = 0; i < longs.length; i++) {  

            longs[i] = new volatilelong();  

        final long start = system.nanotime();  

        runtest();  

        system.out.println("duration = " + (system.nanotime() - start));  

    private static void runtest() throws interruptedexception {  

        thread[] threads = new thread[num_threads];  

        for (int i = 0; i < threads.length; i++) {  

            threads[i] = new thread(new falsesharing(i));  

        for (thread t : threads) {  

            t.start();  

            t.join();  

    public void run() {  

        long i = iterations + 1;  

        while (0 != --i) {  

            longs[arrayindex].value = i;  

    public final static class volatilelong {  

        public volatile long value = 0l;  

        public long p1, p2, p3, p4, p5, p6; // 注释  

代码的逻辑是默认4个线程修改一数组不同元素的内容.  元素的类型是volatilelong, 只有一个长整型成员value和6个没用到的长整型成员. value设为volatile是为了让value的修改所有线程都可见. 在一台westmere(xeon e5620 8core*2)机器上跑一下看

shell代码

$ java falsesharing  

starting....  

duration = 9316356836  

把以上代码49行注释掉, 看看结果:

duration = 59791968514  

# 设置捕捉l2缓存in事件  

$ sudo  opcontrol --setup --event=l2_lines_in:100000  

# 清空工作区  

$ sudo opcontrol --reset  

# 开始捕捉  

$ sudo opcontrol --start  

# 运行程序  

# 程序跑完后, dump捕捉到的数据  

$ sudo opcontrol --dump  

# 停止捕捉  

$ sudo opcontrol -h  

# 报告结果  

$ opreport -l `which java`  

比较一下两个版本的结果, 慢的版本:

cpu: intel westmere microarchitecture, speed 2400.2 mhz (estimated)  

counted l2_lines_in events (l2 lines alloacated) with a unit mask of 0x07 (any l2 lines alloacated) count 100000  

samples  %        image name               symbol name  

34085    99.8447  anon (tgid:18051 range:0x7fcdee53d000-0x7fcdee7ad000) anon (tgid:18051 range:0x7fcdee53d000-0x7fcdee7ad000)  

51        0.1494  anon (tgid:16054 range:0x7fa485722000-0x7fa485992000) anon (tgid:16054 range:0x7fa485722000-0x7fa485992000)  

2         0.0059  anon (tgid:2753 range:0x7f43b317e000-0x7f43b375e000) anon (tgid:2753 range:0x7f43b317e000-0x7f43b375e000)  

快的版本:

22       88.0000  anon (tgid:18873 range:0x7f3e3fa8a000-0x7f3e3fcfa000) anon (tgid:18873 range:0x7f3e3fa8a000-0x7f3e3fcfa000)  

3        12.0000  anon (tgid:2753 range:0x7f43b317e000-0x7f43b375e000) anon (tgid:2753 range:0x7f43b317e000-0x7f43b375e000)  

慢的版本由于false sharing引发的l2缓存in事件达34085次, 而快版本的为0次.总结

伪共享在多核编程中很容易发生, 而且比较隐蔽. 例如, 在jdk的linkedblockingqueue中, 存在指向队列头的引用head和指向队列尾的引用last. 而这种队列经常在异步编程中使有,这两个引用的值经常的被不同的线程修改, 但它们却很可能在同一个缓存行, 于是就产生了伪共享. 线程越多, 核越多,对性能产生的负面效果就越大.

某些java编译器会将没有使用到的补齐数据, 即示例代码中的6个长整型在编译时优化掉, 可以在程序中加入一些代码防止被编译优化.

public static long preventfromoptimization(volatilelong v) {  

    return v.p1 + v.p2 + v.p3 + v.p4 + v.p5 + v.p6;  

}  

2012年4月19日更新:

发现netty和grizzly的代码中的linkedtransferqueue中都使用了paddedatomicreference<qnode>来代替原来的node, 使用了补齐的办法解决了队列伪共享的问题. 不知道是不是jsr-166的人开发的, 看来他们早就意识到这个问题了. 但是从doug lea jsr-166的cvs看不到这个变化, 不知道究竟是谁改的? 他们的repository到底是在哪?