寫Java也得了解CPU--僞共享

第一次接觸僞共享的概念,是在馬丁的博客上;而ifeve也把這一系列博文翻譯整理好了。概讀了幾回,感受到此概念的重要。所以有了這個系列的第二篇讀後總結。html

 

1. 什麼是僞共享(False sharing)

上一篇博文知道,緩存的存儲方式,是以緩存行(Cache Line)爲單位的。通常緩存行的大小是64字節。這意味着,小於64字節的變量,是有可能存在於同一條緩存行的。例如變量X大小32字節,變量Y大小32字節,那麼他們有可能會存在於一條緩存行上。java

根據馬丁博客上的定義,僞共享,就是多個線程同時修改共享在同一個緩存行裏的獨立變量,無心中影響了性能git

 

2.僞共享是怎麼發生的

藉助馬丁的圖,咱們能夠窺知僞共享發生的過程。github

 

 

當核心1上的線程想更新X,而核心2上的線程想更新Y,而X變量和Y變量在同一個緩存行中時;每一個線程都要去競爭緩存行的全部權來更新變量。若是核心1得到所緩存行的全部權,那麼緩存子系統將會使核心2中對應的緩存行失效,反之亦然。這會來來回回的通過L3緩存,大大影響了性能。這種狀況,就像多個線程同事競爭鎖的全部權同樣。若是互相競爭的核心位於不一樣的插槽,就要額外橫跨插槽鏈接,問題可能更加嚴重。緩存

 

3. 怎麼發現僞共享

很遺憾,沒有特別直接有效的方法。馬丁本身也認可,僞共享至關難發現,所以有「無聲性能殺手」之稱。但這不意味着沒法發現。經過觀察L2和L3的緩存命中和丟失的狀況,能夠從側面發現是否有僞共享的發生。多線程

 

4. 怎麼解決僞共享

對於僞共享這種影響性能的問題,解決是關鍵。解決僞共享的方法是經過補齊(Padding),使得每一條緩存行只存一個多線程變量。請看下面的代碼:工具

public final class FalseSharing implements Runnable {
    public final static int NUM_THREADS = 2; // 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 {
        
        //test the size of VolatileLong
        System.out.println(ClassLayout.parseClass(VolatileLong.class).toPrintable());

        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 in order to trigger false sharing
    }
    

}

 

改變線程的數量以及運行此程序(基於Intel Xeon E31270 8GB/64bit Win7/64bit jdk 6),會獲得如下的結果:佈局

這個結果沒有馬丁的結果那麼驚人,僞共享在3-4線程的時候會比較明顯地影響性能。這個結果後面還會繼續分析。然而這並非個完美的測試,由於咱們不能肯定這些VolatileLong會佈局在內存的什麼位置。它們是獨立的對象。可是經驗告訴咱們同一時間分配的對象趨向集中於一塊。性能

 

5.一些思考

上面是馬丁對僞共享的初步解釋。說實話,解釋得略微簡略了一點。讀了幾回,仍是有不太明白的地方,所以厚着臉皮在這裏發了個帖子問點疑惑,結果獲得馬丁本人,Nitsan和Peter等衆大神的回答,收益匪淺。下面摘錄點個人疑惑和他們的解答,更好的理解僞共享:測試

 

5.1 上文得知,L1緩存和L2緩存是核心私有的緩存,不一樣的核心並不共享L1和L2緩存。爲何核心1更新它本身L1上的X,而核心2更新它本身L1上的Y,會發生僞共享?

要回答這個問題,首先得稍微瞭解CPU緩存工做的協議,MESI。這套協議是用來保證CPU緩存的一致性的(cache coherency)。簡單來講,這協議定義了多級緩存下的同一個變量改變後,該怎麼辦。這套協議至關複雜,這裏只是介紹僞共享相關的知識點,來回答咱們的問題。

咱們知道,緩存的最小使用單位,是緩存行。如上面所假設,變量X和變量Y不幸在同一個緩存行裏,而核心1須要X,核心2須要Y。這時候,核心1就會拷貝這條緩存行到本身的L1,核心2也同樣。因此這條緩存行在L3,核心1的L1和核心2的L1裏,正如上圖所示。

假設核心1修改變量X,那麼根據MESI協議,這個緩存行的狀態就會變成M(Modified),表面這一行數據和內存數據不一致,得回寫(write back)此緩存行到L3裏。而這時,須要發送一個Request For Ownership (RFO),來得到L3的這條緩存行的全部權。因爲X和Y在同一條緩存行,雖然核心2修改的變量是Y,但也須要作一樣的事情-發送RFO得到L3同一條緩存行的全部權。所以,僞共享就這樣在L3裏發生了。

 

5.2 補齊(Padding)會不會有失效的時候?

會的。Java 7淘汰或是從新排列了無用的字段,所以上述的補齊在Java 7裏已經失效了,僞共享還會發生。要避免僞共享,須要改變補齊的方式以下:

 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;
    }

這個方法的由來在這裏,並不打算深究。要注意的是,這個方法sumPaddingToPreventOptimisation只是用來防止JVM7消除無用的字段。

 

5.3 怎麼計算VolatileLong的大小?爲何這樣補齊可使它符合緩存行64字節大小?

理論上,咱們知道在64bit Hotspot JVM下,一個long佔8字節之類的知識。但實際的對象佔多少字節,怎麼分佈,得依靠這個工具--JOL來測量。下面是補齊前和補齊後,VolatileLong的輸出:

VolatileLong object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (alignment/padding gap) N/A
16 8 long VolatileLong.value N/A
Instance size: 24 bytes (estimated, the sample instance is not available)
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

VolatileLong object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (alignment/padding gap) N/A
16 8 long VolatileLong.value N/A
24 8 long VolatileLong.p1 N/A
32 8 long VolatileLong.p2 N/A
40 8 long VolatileLong.p3 N/A
48 8 long VolatileLong.p4 N/A
56 8 long VolatileLong.p5 N/A
64 8 long VolatileLong.p6 N/A
Instance size: 72 bytes (estimated, the sample instance is not available)
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

這樣,咱們能夠看到補齊前,VolatileLong只有24字節小於緩存行大小,補齊後就超過緩存行大小。

 

5.4 補齊的對象超過了緩存行,有沒有影響會不會和接下來的變量發生潛在的僞共享?

假設補齊後,VolatileLong是72字節,緊接着恰好有一個變量Z是恰好56個字節,那麼第二個緩存行存放着VolatileLong的8字節那一部分,以及變量Z。那麼同時訪問VolatileLong和Z,會不會發生僞共享呢?是否是必定要補齊到緩存行大小才徹底避免僞共享呢?

答案是否認的,補齊超過緩存行,最多浪費點珍貴的緩存,但不會產生僞共享。請看下面的圖:

| 8b | 16 | 24 | 32 | 40 | 48 | 56 | 64 |

| *  | *  | OH | OH | P   | P   | P    | P    |
| P   | P   | P    | V    | P    | P    | P    | P   |
| P   | P   | P    | *   |  *   |  *   |  *   |  *   |
 
如圖所示,補齊的對象橫跨3個緩存行時,咱們須要的改變的變量,僅僅是V,只要保證V這個變量所在的同一條緩存行內,沒有另一個須要改變的變量,那麼僞共享不會發生。
補充一句,Nitsan大神的補齊對象在 這裏。沒細看,但估計補齊得更加完美。
 

5.5 爲什麼上述測試8線程的結果回比4線程的結果要好?

這是由於,用來測試的機器的CPU只是4核心,僞共享的發生是在L3緩存。若是8線程的話,其實有線程是共享了L1緩存。
 

6. 最後

僞共享是實實在在的問題,並且至關隱蔽。但如今Java的多線程庫,都會考慮到這個問題。例如Disruptor還有Netty都會使用Padding的類代替原有類,達到去除僞共享的目的。
 
 
參考:
 

本文完

相關文章
相關標籤/搜索