鍍金池/ 教程/ Java/ 偽共享(False Sharing)
Ringbuffer的特別之處
寫入 Ringbuffer
鎖的缺點
神奇的緩存行填充
如何從Ringbuffer讀取
Disruptor Wizard已死,Disruptor Wizard永存!
線程間共享數(shù)據(jù)無需競爭
LMAX Disruptor——一個高性能、低延遲且簡單的框架
通過 Axon 和 Disruptor處理 1M tps
揭秘內(nèi)存屏障
Disruptor (無鎖并發(fā)框架)-發(fā)布
LMAX架構(gòu)
偽共享(False Sharing)
解析 Disruptor 的依賴關(guān)系
Disruptor 2.0 更新摘要

偽共享(False Sharing)

原文地址:http://ifeve.com/false-sharing/

作者:Martin Thompson 譯者:丁一

緩存系統(tǒng)中是以緩存行(cache line)為單位存儲的。緩存行是 2 的整數(shù)冪個連續(xù)字節(jié),一般為 32-256 個字節(jié)。最常見的緩存行大小是 64個字節(jié)。

當多線程修改互相獨立的變量時,如果這些變量共享同一個緩存行,就會無意中影響彼此的性能,這就是偽共享。緩存行上的寫競爭是運行在 SMP 系統(tǒng)中并行線程實現(xiàn)可伸縮性最重要的限制因素。有人將偽共享描述成無聲的性能殺手,因為從代碼中很難看清楚是否會出現(xiàn)偽共享。

為了讓可伸縮性與線程數(shù)呈線性關(guān)系,就必須確保不會有兩個線程往同一個變量或緩存行中寫。兩個線程寫同一個變量可以在代碼中發(fā)現(xiàn)。為了確定互相獨立的變量是否共享了同一個緩存行,就需要了解內(nèi)存布局,或找個工具告訴我們。Intel VTune 就是這樣一個分析工具。本文中我將解釋 Java 對象的內(nèi)存布局以及我們該如何填充緩存行以避免偽共享。

http://wiki.jikexueyuan.com/project/disruptor-getting-started/images/3-1.png" alt="" />

圖 1.

圖1說明了偽共享的問題。在核心 1 上運行的線程想更新變量 X,同時核心 2 上的線程想要更新變量 Y。不幸的是,這兩個變量在同一個緩存行中。每個線程都要去競爭緩存行的所有權(quán)來更新變量。如果核心 1 獲得了所有權(quán),緩存子系統(tǒng)將會使核心 2 中對應的緩存行失效。當核心 2 獲得了所有權(quán)然后執(zhí)行更新操作,核心 1 就要使自己對應的緩存行失效。這會來來回回的經(jīng)過 L3 緩存,大大影響了性能。如果互相競爭的核心位于不同的插槽,就要額外橫跨插槽連接,問題可能更加嚴重。

Java 內(nèi)存布局(Java Memory Layout)

對于 HotSpot JVM,所有對象都有兩個字長的對象頭。第一個字是由 24 位哈希碼和 8 位標志位(如鎖的狀態(tài)或作為鎖對象)組成的 Mark Word。第二個字是對象所屬類的引用。如果是數(shù)組對象還需要一個額外的字來存儲數(shù)組的長度。每個對象的起始地址都對齊于 8 字節(jié)以提高性能。因此當封裝對象的時候為了高效率,對象字段聲明的順序會被重排序成下列基于字節(jié)大小的順序:

  1. doubles (8) 和 longs (8)
  2. ints (4) 和 floats (4)
  3. shorts (2) 和 chars (2)
  4. booleans (1) 和 bytes (1)
  5. references (4/8)
  6. <子類字段重復上述順序> (譯注:更多 HotSpot 虛擬機對象結(jié)構(gòu)相關(guān)內(nèi)容:http://www.infoq.com/cn/articles/jvm-hotspot

了解這些之后就可以在任意字段間用 7 個long來填充緩存行。在 Disruptor 里我們對 RingBuffer 的 cursor 和 BatchEventProcessor 的序列進行了緩存行填充。

為了展示其性能影響,我們啟動幾個線程,每個都更新它自己獨立的計數(shù)器。計數(shù)器是 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
    }
}

結(jié)果(Results)

運行上面的代碼,增加線程數(shù)以及添加/移除緩存行的填充,下面的圖 2 描述了我得到的結(jié)果。這是在我 4 核 Nehalem 上測得的運行時間。

http://wiki.jikexueyuan.com/project/disruptor-getting-started/images/3-2.png" alt="" />

圖 2

從不斷上升的測試所需時間中能夠明顯看出偽共享的影響。沒有緩存行競爭時,我們幾近達到了隨著線程數(shù)的線性擴展。

這并不是個完美的測試,因為我們不能確定這些 VolatileLong 會布局在內(nèi)存的什么位置。它們是獨立的對象。但是經(jīng)驗告訴我們同一時間分配的對象趨向集中于一塊。

所以你也看到了,偽共享可能是無聲的性能殺手。

注意:更多偽共享相關(guān)的內(nèi)容,請閱讀我后續(xù) blog。

原創(chuàng)文章,轉(zhuǎn)載請注明: 轉(zhuǎn)載自并發(fā)編程網(wǎng) – ifeve.com

本文鏈接地址: 偽共享(False Sharing)