僞共享(False Sharing)

緩存系統中是以緩存行(cache line)爲單位存儲的。緩存行是2的整數冪個連續字節,通常爲32-256個字節。最多見的緩存行大小是64個字節。當多線程修改互相獨立的變量時,若是這些變量共享同一個緩存行,就會無心中影響彼此的性能,這就是僞共享。緩存行上的寫競爭是運行在SMP系統中並行線程實現可伸縮性最重要的限制因素。有人將僞共享描述成無聲的性能殺手,由於從代碼中很難看清楚是否會出現僞共享。java

爲了讓可伸縮性與線程數呈線性關係,就必須確保不會有兩個線程往同一個變量或緩存行中寫。兩個線程寫同一個變量能夠在代碼中發現。爲了肯定互相獨立的變量是否共享了同一個緩存行,就須要瞭解內存佈局,或找個工具告訴咱們。Intel VTune就是這樣一個分析工具。本文中我將解釋Java對象的內存佈局以及咱們該如何填充緩存行以免僞共享。數組

cache-line.png
圖 1.

圖1說明了僞共享的問題。在覈心1上運行的線程想更新變量X,同時核心2上的線程想要更新變量Y。不幸的是,這兩個變量在同一個緩存行中。每一個線程都要去競爭緩存行的全部權來更新變量。若是核心1得到了全部權,緩存子系統將會使核心2中對應的緩存行失效。當核心2得到了全部權而後執行更新操做,核心1就要使本身對應的緩存行失效。這會來來回回的通過L3緩存,大大影響了性能。若是互相競爭的核心位於不一樣的插槽,就要額外橫跨插槽鏈接,問題可能更加嚴重。緩存

Java內存佈局(Java Memory Layout)多線程

對於HotSpot JVM,全部對象都有兩個字長的對象頭。第一個字是由24位哈希碼和8位標誌位(如鎖的狀態或做爲鎖對象)組成的Mark Word。第二個字是對象所屬類的引用。若是是數組對象還須要一個額外的字來存儲數組的長度。每一個對象的起始地址都對齊於8字節以提升性能。所以當封裝對象的時候爲了高效率,對象字段聲明的順序會被重排序成下列基於字節大小的順序:jvm

  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虛擬機對象結構相關內容:http://www.infoq.com/cn/articles/jvm-hotspot工具

瞭解這些以後就能夠在任意字段間用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)測試

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

duration.png
圖 2.

從不斷上升的測試所需時間中可以明顯看出僞共享的影響。沒有緩存行競爭時,咱們幾近達到了隨着線程數的線性擴展。

這並非個完美的測試,由於咱們不能肯定這些VolatileLong會佈局在內存的什麼位置。它們是獨立的對象。可是經驗告訴咱們同一時間分配的對象趨向集中於一塊。

因此你也看到了,僞共享多是無聲的性能殺手

相關文章
相關標籤/搜索