太牛逼了!項目中用了Disruptor之後,性能提升了2.5倍

作者:jitwxs

https://jitwxs.cn/13836b16.html

存儲設備往往是速度越快價格越昂貴,速度越快價格越低廉。在計算機中,CPU 的速度遠高於主存的速度,而主存的速度又遠高於磁盤的速度。爲了解決不同存儲部件的速度不對等問題,讓高速設備充分發揮性能,引入了多級緩存機制。

爲了解決內存和 CPU 的速度不匹配問題,相繼引入了 L1 Cache、L2 Cache、L3 Cache,數字越小,容量越小,速度越快,位置越接近 CPU。

圖片

現在的 CPU 都是由多個處理器,每個處理器由多個核心構成。一個處理器對應一個物理插槽,不同的處理器間通過 QPI 總線相連。一個處理器間的多核共享 L3 Cache。一個核包含寄存器、L1 Cache、L2 Cache,下圖是Intel Sandy Bridge CPU架構:

圖片

緩存行與僞共享

緩存中的數據並不是獨立的進行存儲的,它的最小存儲單位是緩存行,緩存行的大小是2的整數冪個字節,最常見的緩存行大小是 64 字節。CPU 爲了執行的高效,會在讀取某個對象時,從內存上加載 64 的整數倍的長度,來補齊緩存行。

以 Java 的 long 類型爲例,它是 8 個字節,假設我們存在一個長度爲 8 的 long 數組 arr,那麼CPU 在讀取 arr[0] 時,首先查詢緩存,緩存沒有命中,緩存就會去內存中加載。由於緩存的最小存儲單位是緩存行,64 字節,且數組的內存地址是連續的,則將 arr[0] 到 arr[7] 加載到緩存中。後續 CPU 查詢 arr[6] 時候也可以直接命中緩存。

圖片

現在假設多線程情況下,線程 A 的執行者 CPU Core-1 讀取 arr[1],首先查詢緩存,緩存沒有命中,緩存就會去內存中加載。從內存中讀取 arr[1] 起的連續的 64 個字節地址到緩存中,組成緩存行。由於從arr[1] 起,arr 的長度不足夠 64 個字節,只夠 56 個字節。假設最後 8 個字節內存地址上存儲的是對象 bar,那麼對象 bar 也會被一起加載到緩存行中。

圖片

現在有另一個線程 B,線程 B 的執行者 CPU Core-2 去讀取對象 bar,首先查詢緩存,發現命中了,因爲 Core-1 在讀取 arr 數組的時候也順帶着把 bar 加載到了緩存中。

這就是緩存行共享,聽起來不錯,但是一旦牽扯到了寫入操作就不妙了。

假設 Core-1 想要更新 arr[7] 的值,根據 CPU 的 MESI 協議,那麼它所屬的緩存行就會被標記爲失效。因爲它需要告訴其他的 Core,這個 arr[7] 的值已經被更新了,緩存已經不再準確了,你必須得重新去內存拉取。但是由於緩存的最小單元是緩存行,因此只能把 arr[7] 所在的一整行給標識爲失效。

此時 Core-2 就會很鬱悶了,剛剛還能夠從緩存中讀取到對象 bar,現在再讀取卻被告知緩存行失效,必須得去內存重新拉取,延緩了 Core-2 的執行效率。

這就是緩存僞共享問題,兩個毫無關聯的線程執行,一個線程卻因爲另一個線程的操作,導致緩存失效。這兩個線程其實就是對同一緩存行產生了競爭,降低了併發性。

Disruptor 緩存行填充

Disruptor 爲了解決僞共享問題,使用的方法是緩存行填充。這是一種以空間換時間的策略,主要思想就是通過往對象中填充無意義的變量,來保證整個對象獨佔緩存行。

舉個例子,以 Disruptor 中的 Sequence 爲例,在 volatile long value 的前後各放置了 7 個 long 型變量,確保 value 獨佔一個緩存行。

public class Sequence extends RhsPadding {  
    private static final long VALUE_OFFSET;  
      
    static {  
        VALUE_OFFSET = UNSAFE.objectFieldOffset(Value.class.getDeclaredField("value"));  
        ...  
    }  
    ...  
}  
  
class RhsPadding extends Value {  
    protected long p9, p10, p11, p12, p13, p14, p15;  
}  
  
class Value extends LhsPadding {  
    protected volatile long value;  
}  
  
class LhsPadding {  
    protected long p1, p2, p3, p4, p5, p6, p7;  
}  

如下圖所示,其中 V 就是 Value 類的 value,P 爲 value 前後填充的無意義 long 型變量,U 爲其它無關的變量。不論什麼情況下,都能保證 V 不和其他無關的變量處於同一緩存行中,這樣 V 就不會被其他無關的變量所影響。

圖片

Padding 填充

這裏的 V 也不限定爲 long 類型,其實只要對象的大小大於等於8個字節,通過前後各填充 7 個 long 型變量,就一定能夠保證獨佔緩存行。

此處以 Disruptor 的 RingBuffer 爲例,最左邊的 7 個 long 型變量被定義在頂級父類 RingBufferPad 中,最右邊的 7 個 long 型變量被定義在 RingBuffer 的最後一行變量定義中,這樣所有的需要獨佔的變量都被左右 long 型給包圍,確保會獨佔緩存行。

public final class RingBuffer<E> extends RingBufferFields<E> implements Cursored, EventSequencer<E>, EventSink<E> {  
    public static final long INITIAL_CURSOR_VALUE = Sequence.INITIAL_VALUE;  
    protected long p1, p2, p3, p4, p5, p6, p7;  
    ...  
}  
  
abstract class RingBufferFields<E> extends RingBufferPad  
{  
    ...  
}  
  
abstract class RingBufferPad {  
    protected long p1, p2, p3, p4, p5, p6, p7;  
}  

@Contended

在 JDK 1.8 中,提供了 @sun.misc.Contended 註解,使用該註解就可以讓變量獨佔緩存行,不再需要手動填充了。注意,JVM 需要添加參數 -XX:-RestrictContended 才能開啓此功能。

如果該註解被定義在了類上,表示該類的每個變量都會獨佔緩存行;如果被定義在了變量上,通過指定 groupName,相同的 groupName 會獨佔同一緩存行。

// 類前加上代表整個類的每個變量都會在單獨的cache line中  
@sun.misc.Contended  
public class ContendedData {  
    int value;  
    long modifyTime;  
    boolean flag;  
    long createTime;  
    char key;  
}  
  
// 同一 groupName 在同一緩存行  
public class ContendedGroupData {  
    @sun.misc.Contended("group1")  
    int value;  
    @sun.misc.Contended("group1")  
    long modifyTime;  
    @sun.misc.Contended("group2")  
    boolean flag;  
    @sun.misc.Contended("group3")  
    long createTime;  
    @sun.misc.Contended("group3")  
    char key;  
}  

@Contended 在 JDK 源碼中已經有所應用,以 Thread 類爲例,爲了保證多線程情況下隨機數的操作不會產生僞共享,相關的變量被設置爲同一 groupName。

public class Thread implements Runnable {  
    ...  
    // The following three initially uninitialized fields are exclusively  
    // managed by class java.util.concurrent.ThreadLocalRandom. These  
    // fields are used to build the high-performance PRNGs in the  
    // concurrent code, and we can not risk accidental false sharing.  
    // Hence, the fields are isolated with @Contended.  
  
    /** The current seed for a ThreadLocalRandom */  
    @sun.misc.Contended("tlr")  
    long threadLocalRandomSeed;  
  
    /** Probe hash value; nonzero if threadLocalRandomSeed initialized */  
    @sun.misc.Contended("tlr")  
    int threadLocalRandomProbe;  
  
    /** Secondary seed isolated from public ThreadLocalRandom sequence */  
    @sun.misc.Contended("tlr")  
    int threadLocalRandomSecondarySeed;  
      
    ...  
}  

速度測試

將 volatile long value 封裝爲對象,四線程並行,每個線程循環 1 億次,對 value 進行更新操作,測試緩存行對速度的影響。

CPU:AMD 3600 3.6 GHz,Memory:16 GB

推薦閱讀




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章