CPU緩存與僞共享(false sharing)

轉載自雲棲社區 - Java中的僞共享以及應對方案

什麼是僞共享

CPU緩存系統中是以緩存行(cache line)爲單位存儲的。目前主流的CPU Cache 的 Cache Line 大小都是64Bytes。在多線程情況下,如果需要修改“共享同一個緩存行的變量”,就會無意中影響彼此的性能,這就是僞共享(False Sharing)。

CPU的三級緩存

由於CPU的速度遠遠大於內存速度,所以CPU設計者們就給CPU加上了緩存(CPU Cache)。 以免運算被內存速度拖累。CPU Cache 分成了三個級別:L1,L2,L3。級別越小越接近CPU, 所以速度也更快, 同時也代表着容量越小。
cache
CPU獲取數據會依次從L1,L2,L3中查找,如果都找不到則會直接向內存查找。

緩存行

由於共享變量在CPU緩存中的存儲是以緩存行爲單位,一個緩存行可以存儲多個變量(存滿當前緩存行的字節數),而CPU對緩存的修改又是以緩存行爲最小單位的,那麼就會出現上訴的僞共享問題。

Cache Line 可以簡單的理解爲 CPU Cache 中的最小緩存單位。當你讀一個特定的內存地址,整個緩存行將從主存換入緩存,並且訪問同一個緩存行內的其它值的開銷是很小的。

看如下代碼示例:

int[] arr = new int[64 * 1024 * 1024];
long start = System.nanoTime();
for (int i = 0; i < arr.length; i++) {
    arr[i] *= 3;
}
System.out.println(System.nanoTime() - start);

long start2 = System.nanoTime();
for (int i = 0; i < arr.length; i += 16) {
    arr[i] *= 3;
}
System.out.println(System.nanoTime() - start2);

表面上看,第二個循環工作量爲第一個循環的1/16;但是執行時間是相差不大的,假設在內存規整的情況下,每16個int 佔用4*16=64字節,正好一個緩存行,也就是說這兩個循環訪問內存的次數是一致的。導致耗時相差不大。

緩存關聯性

目前常用的緩存設計是N路組關聯(N-Way Set Associative Cache),他的原理是把一個緩存按照N個 Cache Line 作爲一組(Set),緩存按組劃爲等分。每個內存塊能夠被映射到相對應的set中的任意一個緩存行中。比如一個16路緩存,16個 Cache Line 作爲一個Set,每個內存塊能夠被映射到Set中的16個 CacheLine 的任意一個。一般地,具有一定相同低bit位地址的內存塊將共享同一個Set。
下圖爲一個 2-Way 的 Cache。由圖中可以看到 Main Memory 中的 Index 0,2,4都映射在 Way 0 的不同 CacheLine 中,Index 1,3,5都映射在 Way 1 的不同 CacheLine 中。
2-way

MESI協議

多核CPU都有自己的專有緩存(一般爲L1,L2),以及同一個CPU插槽之間的核共享的緩存(一般爲L3)。不同核心的CPU緩存中難免會加載同樣的數據,那麼如何保證數據的一致性呢,就是MESI協議了。

在MESI協議中,每個Cache line有4個狀態,可用2個bit表示,它們分別是:

  • M(Modified):這行數據有效,數據被修改了,和內存中的數據不一致,數據只存在於本Cache中
  • E(Exclusive):這行數據有效,數據和內存中的數據一致,數據只存在於本Cache中
  • S(Shared):這行數據有效,數據和內存中的數據一致,數據存在於很多Cache中
  • I(Invalid):這行數據無效

那麼,假設有一個變量i=3(應該是包括變量i的緩存塊,塊大小爲緩存行大小);已經加載到多核(a,b,c)的緩存中,此時該緩存行的狀態爲S;此時其中的一個核a改變了變量i的值,那麼在覈a中的當前緩存行的狀態將變爲M,b,c核中的當前緩存行狀態將變爲I。如下圖:
MESI

僞共享問題

那麼爲什麼會出現僞共享問題呢?上訴的情況再擴展一下,假設在多線程情況下,x,y兩個共享變量在同一個緩存行中,核a修改變量x,會導致核b,核c中的x變量和y變量同時失效。

此時對於在覈a上運行的線程,僅僅只是修改了了變量x,卻導致同一個緩存行中的所有變量都無效,需要重新刷緩存(並不一定代表每次都要從內存中重新載入,也有可能是從其他Cache中導入數據,具體的實現要看各個芯片廠商的實現了)。

假設此時在覈b上運行的線程,正好想要修改變量Y,那麼就會出現相互競爭,相互失效的情況,這就是僞共享。

Java對於僞共享的傳統解決方案

在Java 7 之前,可以通過以下方式進行填充解決僞共享的問題。但Java 7 開始會淘汰或重新排列無用字段,因此需採用其它填充方式。

public final class FalseSharing implements Runnable {
    private final static int NUM_THREADS = 4; // change
    private 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;
    }
}

Java 8 中的解決方案

Java8中新增了一個註解:@sun.misc.Contended。加上這個註解的類會自動補齊緩存行,需要注意的是此註解默認是無效的,需要在jvm啓動時設置 -XX:-RestrictContended 纔會生效。

運行結果:

@sun.misc.Contended
public final static class VolatileLong {
    public volatile long value = 0L;
    //public long p1, p2, p3, p4, p5, p6;
}
duration = 8987991013

參考文獻:

  1. http://igoro.com/archive/gallery-of-processor-cache-effects/
  2. http://ifeve.com/false-sharing/http://ifeve.com/false-sharing/
  3. http://blog.csdn.net/muxiqingyang/article/details/6615199http://blog.csdn.net/muxiqingyang/article/details/6615199
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章