无声的性能杀手——伪共享(False Sharing)

性能杀手

缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,通常为32-256个字节。最多见的缓存行大小是64个字节。当多线程修改互相独立的变量时,若是这些变量共享同一个缓存行,就会无心中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素。有人将伪共享描述成无声的性能杀手,由于从代码中很难看清楚是否会出现伪共享。数组

为了让可伸缩性与线程数呈线性关系,就必须确保不会有两个线程往同一个变量或缓存行中写。两个线程写同一个变量能够在代码中发现。为了肯定互相独立的变量是否共享了同一个缓存行,就须要了解内存布局,或找个工具告诉咱们。Intel VTune就是这样一个分析工具。本文中我将解释Java对象的内存布局以及咱们该如何填充缓存行以免伪共享。
无声的性能杀手——伪共享(False Sharing)缓存

上图说明了伪共享的问题。在Core1上运行的线程想更新变量X,同时Core2上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每一个线程都要去竞争缓存行的全部权来更新变量。若是Core1得到了全部权,缓存子系统将会使Core2中对应的缓存行失效。当Core2得到了全部权而后执行更新操做,Core1就要使本身对应的缓存行失效。这会来来回回的通过L3缓存,大大影响了性能。若是互相竞争的核心位于不一样的插槽,就要额外横跨插槽链接,问题可能更加严重。多线程

Java内存布局(Java Memory Layout)

对于HotSpot JVM,全部对象都有两个字长的对象头。第一个字是由24位哈希码和8位标志位(如锁的状态或做为锁对象)组成的Mark Word。第二个字是对象所属类的引用。若是是数组对象还须要一个额外的字来存储数组的长度。每一个对象的起始地址都对齐于8字节以提升性能。所以当封装对象的时候为了高效率,对象字段声明的顺序会被重排序成下列基于字节大小的顺序:ide

1.doubles (8) 和 longs (8)
2.ints (4) 和 floats (4)
3.shorts (2) 和 chars (2)工具

  1. booleans (1) 和 bytes (1)
  2. references (4/8)
  3. <子类字段重复上述顺序>
    了解这些以后就能够在任意字段间用7个long来填充缓存行。在Disruptor里咱们对RingBuffer的cursor和BatchEventProcessor的序列进行了缓存行填充。

为了展现其性能影响,咱们启动几个线程,每一个都更新它本身独立的计数器。计数器是volatile long类型的,因此其它线程能看到它们的进展。布局

public final class FalseSharing

    implements Runnable

{

    public final static int NUM_THREADS = 4; // change
    public final static long ITERATIONS = 500L * 1000L * 1000L;
    private final int arrayIndex;
    private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];

    static

    {
        for (int i = 0; i < longs.length; i++)
        {
            longs[i] = new VolatileLong();

        }
    }
    public FalseSharing(final int arrayIndex)
    {
        this.arrayIndex = arrayIndex;
    }
    public static void main(final String[] args) throws Exception
    {
        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();
        }
     for (Thread t : threads)
        {
            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; // comment out
    }

}

结果(Results)

运行上面的代码,增长线程数以及添加/移除缓存行的填充,下面的图描述了我获得的结果。这是在我4核Nehalem上测得的运行时间。
无声的性能杀手——伪共享(False Sharing)性能

从不断上升的测试所需时间中可以明显看出伪共享的影响。没有缓存行竞争时,咱们几近达到了随着线程数的线性扩展。测试

这并非个完美的测试,由于咱们不能肯定这些VolatileLong会布局在内存的什么位置。它们是独立的对象。可是经验告诉咱们同一时间分配的对象趋向集中于一块。this

因此你也看到了,伪共享多是无声的性能杀手。线程

相关文章
相关标签/搜索