Disruptor高性能之道-緩存行填充
在上一篇文章中,我們介紹了Disruptor實現(xiàn)高性能的方式之一:環(huán)形數(shù)組RingBuffer。
本文我們介紹 Disruptor高性能的另一個實現(xiàn)機(jī)制為 「“緩存行填充”」,它解決了CPU訪問內(nèi)存變量的“偽共享”問題。
什么是偽共享?
?在解釋什么是偽共享之前,先了解下數(shù)據(jù)在緩存中是如何存儲的。
?
我們都知道,計算機(jī)為了解決CPU與主存之間速度差的問題,引入了多級緩存機(jī)制。

事實上,數(shù)據(jù)在CPU緩存(多級cache)中并非是單獨存儲的,而是按行存儲的。其中每一行成為一個緩存行。

緩存行是CPU的Cache與主內(nèi)存進(jìn)行數(shù)據(jù)交換的基本單位,每個緩存行的大小一般為2的N次方字節(jié)。(「在32位計算機(jī)中為32字節(jié),64位計算機(jī)中為64字節(jié)?!?/strong>)可以想到,如果計算機(jī)為128位,則緩存行大小就是128字節(jié)。
在Java中,一個long型變量為8字節(jié),也就是說在64位計算機(jī)中,每行可存放8個long型變量。
?當(dāng)CPU訪問某個變量的時,如果CPU Cache中存在該變量,則直接獲取。若不存在則去主內(nèi)存獲取該變量。由于緩存行機(jī)制的存在,因此會將該變量所在內(nèi)存區(qū)域為一個緩存行大小的內(nèi)存復(fù)制到CPU Cache中。
?
此時有可能會在一行緩存行中加載多個變量,如圖中不同的顏色對應(yīng)不同的long型變量。

?試想,如果多個內(nèi)核的線程都操作了同一緩存行的數(shù)據(jù),如圖所示。CPU1讀取并修改了緩存行中的變量D,了解volatile的同學(xué)都知道,當(dāng)CPU Cache中的變量發(fā)生變更,會通過緩存一致性協(xié)議通知其他CPU失效當(dāng)前緩存行,重新從主內(nèi)存中加載當(dāng)前行的值。
?

圖中,CPU1修改了緩存行中的變量D,CPU2也在讀取該緩存行的值。根據(jù)緩存一致性協(xié)議,CPU2中的緩存行會失效,因為它操作的緩存行中的變量D的值已經(jīng)不是最新值了。
這是因為CPU是以緩存行為單位進(jìn)行數(shù)據(jù)的讀寫操作的。
這就是偽共享。
為什么是“偽”共享呢?
?看起來CPU1 與 CPU2 共享了同一個緩存行,但是由于CPU以緩存行為單位進(jìn)行讀寫操作,無論CPU1 與 CPU2中的任何一位修改了緩存行中的值,都需要通知其他CPU對失效該緩存行。也就是說當(dāng)線程對緩存進(jìn)行了寫操作,則當(dāng)前線程所在內(nèi)核就需要失效其他內(nèi)核的緩存行,并重新加載主內(nèi)存。
?
這是一種緩存未命中的情況,當(dāng)發(fā)生這樣的情況,緩存本身的意義就被削弱了,因為CPU始終需要從主內(nèi)存加載數(shù)據(jù),而根本命中不了CPU Cache中的緩存。
?所謂的“偽”共享,就可以理解成是一種 “錯誤”的共享,這種共享如果不發(fā)生,則多核CPU操作緩存行互不影響,每個核心都只關(guān)心自己操作的變量,而不會因為讀寫自己關(guān)心的變量而影響到其他CPU對變量的讀寫。
?
Disruptor是如何進(jìn)行緩存行填充的?
?Disruptor解決偽共享的方式為:使用緩存行填充。
?
上文我們提到,由于多核CPU同時讀寫統(tǒng)一緩存行中的數(shù)據(jù),導(dǎo)致了CPU Cache命中失敗的偽共享問題。
那么只需要避免多核CPU同時操作統(tǒng)一緩存行,不就可以解決這個問題了么?
事實上,Disruptor正是這么做的。
?Disruptor為Sequence中的value(volatile修飾)進(jìn)行了緩存行填充,保證每個sequence只在一個緩存行中存在,避免了其他的變量對sequence的干擾。
?
class?LhsPadding
{
????protected?long?p1,?p2,?p3,?p4,?p5,?p6,?p7;??//?緩存行填充
}
class?Value?extends?LhsPadding
{
????protected?volatile?long?value;
}
class?RhsPadding?extends?Value
{
????protected?long?p9,?p10,?p11,?p12,?p13,?p14,?p15;?//?緩存行填充
}
/**
?*?Concurrent?sequence?class?used?for?tracking?the?progress?of
?*?the?ring?buffer?and?event?processors.??Support?a?number
?*?of?concurrent?operations?including?CAS?and?order?writes.
?*
?*?
Also?attempts?to?be?more?efficient?with?regards?to?false
?*?sharing?by?adding?padding?around?the?volatile?field.
?*/
public?class?Sequence?extends?RhsPadding
{
????static?final?long?INITIAL_VALUE?=?-1L;
????private?static?final?Unsafe?UNSAFE;
????private?static?final?long?VALUE_OFFSET;
????static
????{
????????UNSAFE?=?Util.getUnsafe();
????????try
????????{
????????????VALUE_OFFSET?=?UNSAFE.objectFieldOffset(Value.class.getDeclaredField("value"));
????????}
????????catch?(final?Exception?e)
????????{
????????????throw?new?RuntimeException(e);
????????}
????}
其他的緩存行填充機(jī)制
JDK1.8 提供了注解 「@Contended」 用于解決偽共享問題,需要注意的是,如果業(yè)務(wù)代碼需要使用該注解,要添加JVM參數(shù)
?-XX:-RestrictContended。
?
默認(rèn)填充寬度為128,若需要自定義填充寬度,則設(shè)置
?-XX:ContendedPaddingWidth
?
具體的使用方式為:
@sun.misc.Contended
public?final?static?class?Value?{
??public?volatile?long?value?=?0L;
}
參考資料
《Java并發(fā)編程之美》 并發(fā)編程網(wǎng):剖析Disruptor:為什么會這么快?(二)神奇的緩存行填充
