False Sharing 伪共享 – 译

翻译自:http://mechanical-sympathy.blogspot.com/2011/07/false-sharing.html

伪共享

内存在缓存系统里是以缓存行为单位存储的。缓存行是大小为2的整数幂的连续字节,典型是32-256字节。最普遍的缓存行大小是64字节。伪共享是个术语,用于当多个线程不知不觉地相互影响对方的性能,在修改使用相同缓存行的依赖变量时。缓存行上的写冲突是并行执行线程在SMP(对称多处理器 )系统上获得伸缩性的单一最大制约因素。我已听到伪共享被描述为无声的性能杀手,因为在查看代码时,它非常不明显。

为了达到按线程数量的线性伸缩性,我们必须确保没有两个线程写同一个变量或同一个缓存行。两个线程写同一个变量可以在代码层面跟踪到。为了知道独立变量是否共享同一个缓存行,我们需要知道内存布局,或者让工具告诉我们。 Intel VTune是这样的一个分析工具。在这篇文章,我将解释Java对象的内存布局和如何通过填充缓存行来避免伪共享。

false-sharing
上图展示了伪共享的问题。线程A允许在Core 1,想更新变量X,运行在Core 2的线程想更新变量Y。不幸的是这两个热门变量属于同一个缓存行。每个线程将竞争缓存行的所有权,这样它们可以更新它。如果Core 1获得所有权,缓存子系统需要作废Core 2对应的缓存行。当Core 2获得所有权并执行它的更新,Core 1 将被告诉去作废它的缓存行拷贝。这将来来回回往返让L3缓存极大影响性能。如果竞争的Core处于不同的sockets,那么需要额外的跨Socket互连,这个问题将更加恶化。

Java 内存布局

对于HotSpot JVM,所有对象都有一个2-字(world)的头。第一个是“标记(mark)”字,有24比特位用于哈希码和8比特位用于标记,例如锁状态,或交换用于锁对象。第二个字是指向对象所属类的引用。数组有额外的字用于数组的大小。每个对象对齐到8字节的粒度边界,为了性能。因而为了更高效填充,对象的字段将被重排序,从声明的顺序到下面的基于字节大小的顺序:

  1. doubles (8) and longs (8)
  2. ints (4) and floats (4)
  3. shorts (2) and chars (2)
  4. booleans (1) and bytes (1)
  5. references (4/8)
  6. <repeat for sub-class fields>

(译注:上面小括号里的数字表示占用的字节数。)

有了这些知识,我们可以用7个long在任意字段之间填充缓存行。在Disruptor里,我们在RingBuffer游标和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
    }
}

结果

运行上面的代码,逐渐增加线程数量和添加/移除缓存行填充,我得到的结果如下图所示。这是衡量运行在我的4核 Nehalem 上的测试持续时间。
false-sharing-test-result

伪共享的影响是清晰可见的,通过完成测试需要的时间的增加。没有缓存行竞争,我们可以达到接近按线程线性的扩展。

这不是一个完美等测试,因为我们不能确定VolatileLongs将布局到内存的哪里。他们是独立的变量。然而,经验显示同时分配的对象倾向于同地协作(co-located)。

现在你知道,伪共享是无声的性能杀手了。

另一个测试例子

这是来自作者的另一篇文章:http://mechanical-sympathy.blogspot.com/2011/08/false-sharing-java-7.html,由于相关,就放一起了。

看起来Java 7变得更聪明了,会消除或重排序未使用的字段,这重新引入伪共享。作者认为下面的代码是最可靠的:

import java.util.concurrent.atomic.AtomicLong;

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 PaddedAtomicLong[] longs = new PaddedAtomicLong[NUM_THREADS];
    static
    {
        for (int i = 0; i < longs.length; i++)
        {
            longs[i] = new PaddedAtomicLong();
        }
    }

    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].set(i);
        }
    }

    public static long sumPaddingToPreventOptimisation(final int index)
    {
        PaddedAtomicLong v = longs[index];
        return v.p1 + v.p2 + v.p3 + v.p4 + v.p5 + v.p6;
    }

    public static class PaddedAtomicLong extends AtomicLong
    {
        public volatile long p1, p2, p3, p4, p5, p6 = 7L;
    }
}

欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据