第一次接觸僞共享的概念,是在馬丁的博客上;而ifeve也把這一系列博文翻譯整理好了。概讀了幾回,感受到此概念的重要。所以有了這個系列的第二篇讀後總結。html
在上一篇博文知道,緩存的存儲方式,是以緩存行(Cache Line)爲單位的。通常緩存行的大小是64字節。這意味着,小於64字節的變量,是有可能存在於同一條緩存行的。例如變量X大小32字節,變量Y大小32字節,那麼他們有可能會存在於一條緩存行上。java
根據馬丁博客上的定義,僞共享,就是多個線程同時修改共享在同一個緩存行裏的獨立變量,無心中影響了性能。git
藉助馬丁的圖,咱們能夠窺知僞共享發生的過程。github
當核心1上的線程想更新X,而核心2上的線程想更新Y,而X變量和Y變量在同一個緩存行中時;每一個線程都要去競爭緩存行的全部權來更新變量。若是核心1得到所緩存行的全部權,那麼緩存子系統將會使核心2中對應的緩存行失效,反之亦然。這會來來回回的通過L3緩存,大大影響了性能。這種狀況,就像多個線程同事競爭鎖的全部權同樣。若是互相競爭的核心位於不一樣的插槽,就要額外橫跨插槽鏈接,問題可能更加嚴重。緩存
很遺憾,沒有特別直接有效的方法。馬丁本身也認可,僞共享至關難發現,所以有「無聲性能殺手」之稱。但這不意味着沒法發現。經過觀察L2和L3的緩存命中和丟失的狀況,能夠從側面發現是否有僞共享的發生。多線程
對於僞共享這種影響性能的問題,解決是關鍵。解決僞共享的方法是經過補齊(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會佈局在內存的什麼位置。它們是獨立的對象。可是經驗告訴咱們同一時間分配的對象趨向集中於一塊。性能
上面是馬丁對僞共享的初步解釋。說實話,解釋得略微簡略了一點。讀了幾回,仍是有不太明白的地方,所以厚着臉皮在這裏發了個帖子問點疑惑,結果獲得馬丁本人,Nitsan和Peter等衆大神的回答,收益匪淺。下面摘錄點個人疑惑和他們的解答,更好的理解僞共享:測試
要回答這個問題,首先得稍微瞭解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裏發生了。
會的。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消除無用的字段。
理論上,咱們知道在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字節小於緩存行大小,補齊後就超過緩存行大小。
假設補齊後,VolatileLong是72字節,緊接着恰好有一個變量Z是恰好56個字節,那麼第二個緩存行存放着VolatileLong的8字節那一部分,以及變量Z。那麼同時訪問VolatileLong和Z,會不會發生僞共享呢?是否是必定要補齊到緩存行大小才徹底避免僞共享呢?
答案是否認的,補齊超過緩存行,最多浪費點珍貴的緩存,但不會產生僞共享。請看下面的圖:
| 8b | 16 | 24 | 32 | 40 | 48 | 56 | 64 |
本文完