一篇对伪共享、缓存行填充和CPU缓存讲的很透彻的文章
認(rèn)識(shí)CPU Cache
CPU Cache概述
?
隨著CPU的頻率不斷提升,而內(nèi)存的訪(fǎng)問(wèn)速度卻沒(méi)有質(zhì)的突破,為了彌補(bǔ)訪(fǎng)問(wèn)內(nèi)存的速度慢,充分發(fā)揮CPU的計(jì)算資源,提高CPU整體吞吐量,在CPU與內(nèi)存之間引入了一級(jí)Cache。隨著熱點(diǎn)數(shù)據(jù)體積越來(lái)越大,一級(jí)Cache L1已經(jīng)不滿(mǎn)足發(fā)展的要求,引入了二級(jí)Cache L2,三級(jí)Cache L3。(注:若無(wú)特別說(shuō)明,本文的Cache指CPU Cache,高速緩存)CPU Cache在存儲(chǔ)器層次結(jié)構(gòu)中的示意如下圖:
計(jì)算機(jī)早已進(jìn)入多核時(shí)代,軟件也越來(lái)越多的支持多核運(yùn)行。一個(gè)處理器對(duì)應(yīng)一個(gè)物理插槽,多處理器間通過(guò)QPI總線(xiàn)相連。一個(gè)處理器包含多個(gè)核,一個(gè)處理器間的多核共享L3 Cache。一個(gè)核包含寄存器、L1 Cache、L2 Cache,下圖是Intel Sandy Bridge CPU架構(gòu),一個(gè)典型的NUMA多處理器結(jié)構(gòu):
作為程序員,需要理解計(jì)算機(jī)存儲(chǔ)器層次結(jié)構(gòu),它對(duì)應(yīng)用程序的性能有巨大的影響。如果需要的程序是在CPU寄存器中的,指令執(zhí)行時(shí)1個(gè)周期內(nèi)就能訪(fǎng)問(wèn)到他們。如果在CPU Cache中,需要1~30個(gè)周期;如果在主存中,需要50~200個(gè)周期;在磁盤(pán)上,大概需要幾千萬(wàn)個(gè)周期。充分利用它的結(jié)構(gòu)和機(jī)制,可以有效的提高程序的性能。
以我們常見(jiàn)的X86芯片為例,Cache的結(jié)構(gòu)下圖所示:整個(gè)Cache被分為S個(gè)組,每個(gè)組是又由E行個(gè)最小的存儲(chǔ)單元——Cache Line所組成,而一個(gè)Cache Line中有B(B=64)個(gè)字節(jié)用來(lái)存儲(chǔ)數(shù)據(jù),即每個(gè)Cache Line能存儲(chǔ)64個(gè)字節(jié)的數(shù)據(jù),每個(gè)Cache Line又額外包含一個(gè)有效位(valid bit)、t個(gè)標(biāo)記位(tag bit),其中valid bit用來(lái)表示該緩存行是否有效;tag bit用來(lái)協(xié)助尋址,唯一標(biāo)識(shí)存儲(chǔ)在CacheLine中的塊;而Cache Line里的64個(gè)字節(jié)其實(shí)是對(duì)應(yīng)內(nèi)存地址中的數(shù)據(jù)拷貝。根據(jù)Cache的結(jié)構(gòu)題,我們可以推算出每一級(jí)Cache的大小為B×E×S。
那么如何查看自己電腦CPU的Cache信息呢?
在windows下查看方式有多種方式,其中最直觀的是,通過(guò)安裝CPU-Z軟件,直接顯示Cache信息,如下圖:
此外,Windows下還有兩種方法:
①Windows API調(diào)用GetLogicalProcessorInfo。?
②通過(guò)命令行系統(tǒng)內(nèi)部工具CoreInfo。
如果是Linux系統(tǒng), 可以使用下面的命令查看Cache信息:
ls /sys/devices/system/cpu/cpu0/cache/index0
還有l(wèi)scpu等命令也可以查看相關(guān)信息,如果是Mac系統(tǒng),可以用sysctl machdep.cpu 命令查看cpu信息。
如果我們用Java編程,還可以通過(guò)CacheSize API方式來(lái)獲取Cache信息, CacheSize是一個(gè)谷歌的小項(xiàng)目,java語(yǔ)言通過(guò)它可以進(jìn)行訪(fǎng)問(wèn)本機(jī)Cache的信息。示例代碼如下:
?public static void main(String[] args) throws CacheNotFoundException {
CacheInfo info = CacheInfo.getInstance();
CacheLevelInfo l1Datainf = info.getCacheInformation(CacheLevel.L1, CacheType.DATA_CACHE);
System.out.println("第一級(jí)數(shù)據(jù)緩存信息:"+l1Datainf.toString());
CacheLevelInfo l1Instrinf = info.getCacheInformation(CacheLevel.L1, CacheType.INSTRUCTION_CACHE);
System.out.println("第一級(jí)指令緩存信息:"+l1Instrinf.toString());
}
打印輸出結(jié)果如下:
?第一級(jí)數(shù)據(jù)緩存信息:CacheLevelInfo [cacheLevel=L1, cacheType=DATA_CACHE, cacheSets=64, cacheCoherencyLineSize=64, cachePhysicalLinePartitions=1, cacheWaysOfAssociativity=8, isFullyAssociative=false, isSelfInitializing=true, totalSizeInBytes=32768]
第一級(jí)指令緩存信息:CacheLevelInfo [cacheLevel=L1, cacheType=INSTRUCTION_CACHE, cacheSets=64, cacheCoherencyLineSize=64, cachePhysicalLinePartitions=1, cacheWaysOfAssociativity=8, isFullyAssociative=false, isSelfInitializing=true, totalSizeInBytes=32768]
還可以查詢(xún)L2、L3級(jí)緩存的信息,這里不做示例。從打印的信息和CPU-Z顯示的信息可以看出,本機(jī)的Cache信息是一致的,L1數(shù)據(jù)/指令緩存大小都為:C=B×E×S=64×8×64=32768字節(jié)=32KB。
Cache Line偽共享及解決方案
Cache Line偽共享分析
說(shuō)偽共享前,先看看Cache Line 在java編程中使用的場(chǎng)景。如果CPU訪(fǎng)問(wèn)的內(nèi)存數(shù)據(jù)不在Cache中(一級(jí)、二級(jí)、三級(jí)),這就產(chǎn)生了Cache Line miss問(wèn)題,此時(shí)CPU不得不發(fā)出新的加載指令,從內(nèi)存中獲取數(shù)據(jù)。通過(guò)前面對(duì)Cache存儲(chǔ)層次的理解,我們知道一旦CPU要從內(nèi)存中訪(fǎng)問(wèn)數(shù)據(jù)就會(huì)產(chǎn)生一個(gè)較大的時(shí)延,程序性能顯著降低,所謂遠(yuǎn)水救不了近火。為此我們不得不提高Cache命中率,也就是充分發(fā)揮局部性原理。
局部性包括時(shí)間局部性、空間局部性。時(shí)間局部性:對(duì)于同一數(shù)據(jù)可能被多次使用,自第一次加載到Cache Line后,后面的訪(fǎng)問(wèn)就可以多次從Cache Line中命中,從而提高讀取速度(而不是從下層緩存讀取)??臻g局部性:一個(gè)Cache Line有64字節(jié)塊,我們可以充分利用一次加載64字節(jié)的空間,把程序后續(xù)會(huì)訪(fǎng)問(wèn)的數(shù)據(jù),一次性全部加載進(jìn)來(lái),從而提高Cache Line命中率(而不是重新去尋址讀取)。
看個(gè)例子:內(nèi)存地址是連續(xù)的數(shù)組(利用空間局部性),能一次被L1緩存加載完成。
如下代碼,長(zhǎng)度為16的row和column數(shù)組,在Cache Line 64字節(jié)數(shù)據(jù)塊上內(nèi)存地址是連續(xù)的,能被一次加載到Cache Line中,所以在訪(fǎng)問(wèn)數(shù)組時(shí),Cache Line命中率高,性能發(fā)揮到極致。
?public int run(int[] row, int[] column) {
int sum = 0;
for(int i = 0; i < 16; i++ ) {
sum += row[i] * column[i];
}
return sum;
}
而上面例子中變量i則體現(xiàn)了時(shí)間局部性,i作為計(jì)數(shù)器被頻繁操作,一直存放在寄存器中,每次從寄存器訪(fǎng)問(wèn),而不是從主存甚至磁盤(pán)訪(fǎng)問(wèn)。雖然連續(xù)緊湊的內(nèi)存分配帶來(lái)高性能,但并不代表它一直都能帶來(lái)高性能。如果把它放在多線(xiàn)程中將會(huì)發(fā)生什么呢?如圖:
數(shù)據(jù)X、Y、Z被加載到同一Cache Line中,線(xiàn)程A在Core1修改X,線(xiàn)程B在Core2上修改Y。根據(jù)MESI大法,假設(shè)是Core1是第一個(gè)發(fā)起操作的CPU核,Core1上的L1 Cache Line由S(共享)狀態(tài)變成M(修改,臟數(shù)據(jù))狀態(tài),然后告知其他的CPU核,圖例則是Core2,引用同一地址的Cache Line已經(jīng)無(wú)效了;當(dāng)Core2發(fā)起寫(xiě)操作時(shí),首先導(dǎo)致Core1將X寫(xiě)回主存,Cache Line狀態(tài)由M變?yōu)镮(無(wú)效),而后才是Core2從主存重新讀取該地址內(nèi)容,Cache Line狀態(tài)由I變成E(獨(dú)占),最后進(jìn)行修改Y操作, Cache Line從E變成M??梢?jiàn)多個(gè)線(xiàn)程操作在同一Cache Line上的不同數(shù)據(jù),相互競(jìng)爭(zhēng)同一Cache Line,導(dǎo)致線(xiàn)程彼此牽制影響,變成了串行程序,降低了并發(fā)性。此時(shí)我們則需要將共享在多線(xiàn)程間的數(shù)據(jù)進(jìn)行隔離,使他們不在同一個(gè)Cache Line上,從而提升多線(xiàn)程的性能。
Cache Line偽共享處理方案
處理偽共享的兩種方式:
在Java類(lèi)中,最優(yōu)化的設(shè)計(jì)是考慮清楚哪些變量是不變的,哪些是經(jīng)常變化的,哪些變化是完全相互獨(dú)立的,哪些屬性一起變化。舉個(gè)例子:
?public class Data{
long modifyTime;
boolean flag;
long createTime;
char key;
int value;
}
假如業(yè)務(wù)場(chǎng)景中,上述的類(lèi)滿(mǎn)足以下幾個(gè)特點(diǎn):
當(dāng)上面的對(duì)象需要由多個(gè)線(xiàn)程同時(shí)的訪(fǎng)問(wèn)時(shí),從Cache角度來(lái)說(shuō),就會(huì)有一些有趣的問(wèn)題。當(dāng)我們沒(méi)有加任何措施時(shí),Data對(duì)象所有的變量極有可能被加載在L1緩存的一行Cache Line中。在高并發(fā)訪(fǎng)問(wèn)下,會(huì)出現(xiàn)這種問(wèn)題:
如上圖所示,每次value變更時(shí),根據(jù)MESI協(xié)議,對(duì)象其他CPU上相關(guān)的Cache Line全部被設(shè)置為失效。其他的處理器想要訪(fǎng)問(wèn)未變化的數(shù)據(jù)(key 和 createTime)時(shí),必須從內(nèi)存中重新拉取數(shù)據(jù),增大了數(shù)據(jù)訪(fǎng)問(wèn)的開(kāi)銷(xiāo)。
Padding 方式
正確的方式應(yīng)該將該對(duì)象屬性分組,將一起變化的放在一組,與其他屬性無(wú)關(guān)的屬性放到一組,將不變的屬性放到一組。這樣當(dāng)每次對(duì)象變化時(shí),不會(huì)帶動(dòng)所有的屬性重新加載緩存,提升了讀取效率。在JDK1.8以前,我們一般是在屬性間增加長(zhǎng)整型變量來(lái)分隔每一組屬性。被操作的每一組屬性占的字節(jié)數(shù)加上前后填充屬性所占的字節(jié)數(shù),不小于一個(gè)cache line的字節(jié)數(shù)就可以達(dá)到要求:
?public class DataPadding{
long a1,a2,a3,a4,a5,a6,a7,a8;//防止與前一個(gè)對(duì)象產(chǎn)生偽共享
int value;
long modifyTime;
long b1,b2,b3,b4,b5,b6,b7,b8;//防止不相關(guān)變量偽共享;
boolean flag;
long c1,c2,c3,c4,c5,c6,c7,c8;//
long createTime;
char key;
long d1,d2,d3,d4,d5,d6,d7,d8;//防止與下一個(gè)對(duì)象產(chǎn)生偽共享
}
通過(guò)填充變量,使不相關(guān)的變量分開(kāi)
Contended注解方式
在JDK1.8中,新增了一種注解@sun.misc.Contended,來(lái)使各個(gè)變量在Cache line中分隔開(kāi)。注意,jvm需要添加參數(shù)-XX:-RestrictContended才能開(kāi)啟此功能?
用時(shí),可以在類(lèi)前或?qū)傩郧凹由洗俗⑨?#xff1a;
// 類(lèi)前加上代表整個(gè)類(lèi)的每個(gè)變量都會(huì)在單獨(dú)的cache line中
@sun.misc.Contended
@SuppressWarnings("restriction")
public class ContendedData {
int value;
long modifyTime;
boolean flag;
long createTime;
char key;
}
或者這種:
// 屬性前加上時(shí)需要加上組標(biāo)簽
@SuppressWarnings("restriction")
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;
}
采取上述措施圖示:
JDK1.8 ConcurrentHashMap的處理
java.util.concurrent.ConcurrentHashMap在這個(gè)如雷貫耳的Map中,有一個(gè)很基本的操作問(wèn)題,在并發(fā)條件下進(jìn)行++操作。因?yàn)?#43;+這個(gè)操作并不是原子的,而且在連續(xù)的Atomic中,很容易產(chǎn)生偽共享(false sharing)。所以在其內(nèi)部有專(zhuān)門(mén)的數(shù)據(jù)結(jié)構(gòu)來(lái)保存long型的數(shù)據(jù):
?(openjdk\jdk\src\share\classes\java\util\concurrent\ConcurrentHashMap.java line:2506):
/* ---------------- Counter support -------------- */
/**
* A padded cell for distributing counts. Adapted from LongAdder
* and Striped64. See their internal docs for explanation.
*/
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
我們看到該類(lèi)中,是通過(guò)@sun.misc.Contended達(dá)到防止false sharing的目的
JDK1.8 Thread 的處理
java.lang.Thread在java中,生成隨機(jī)數(shù)是和線(xiàn)程有著關(guān)聯(lián)。而且在很多情況下,多線(xiàn)程下產(chǎn)生隨機(jī)數(shù)的操作是很常見(jiàn)的,JDK為了確保產(chǎn)生隨機(jī)數(shù)的操作不會(huì)產(chǎn)生false sharing ,把產(chǎn)生隨機(jī)數(shù)的三個(gè)相關(guān)值設(shè)為獨(dú)占cache line。
?(openjdk\jdk\src\share\classes\java\lang\Thread.java line:2023)
// 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;
Java中對(duì)Cache line經(jīng)典設(shè)計(jì)
Disruptor框架
認(rèn)識(shí)Disruptor
LMAX是在英國(guó)注冊(cè)并受到FCA監(jiān)管的外匯黃金交易所。也是歐洲第一家也是唯一一家采用多邊交易設(shè)施Multilateral Trading Facility(MTF)擁有交易所牌照和經(jīng)紀(jì)商牌照的歐洲頂級(jí)金融公司。LMAX的零售金融交易平臺(tái),是建立在JVM平臺(tái)上,核心是一個(gè)業(yè)務(wù)邏輯處理器,它能夠在一個(gè)線(xiàn)程里每秒處理6百萬(wàn)訂單。業(yè)務(wù)邏輯處理器的核心就是Disruptor(注,本文Disruptor基于當(dāng)前最新3.3.6版本),這是一個(gè)Java實(shí)現(xiàn)的并發(fā)組件,能夠在無(wú)鎖的情況下實(shí)現(xiàn)網(wǎng)絡(luò)的Queue并發(fā)操作,它確保任何數(shù)據(jù)只由一個(gè)線(xiàn)程擁有以進(jìn)行寫(xiě)訪(fǎng)問(wèn),從而消除寫(xiě)爭(zhēng)用的設(shè)計(jì), 這種設(shè)計(jì)被稱(chēng)作“破壞者”,也是這樣命名這個(gè)框架的。
Disruptor是一個(gè)線(xiàn)程內(nèi)通信框架,用于線(xiàn)程里共享數(shù)據(jù)。與LinkedBlockingQueue類(lèi)似,提供了一個(gè)高速的生產(chǎn)者消費(fèi)者模型,廣泛用于批量IO讀寫(xiě),在硬盤(pán)讀寫(xiě)相關(guān)的程序中應(yīng)用的十分廣泛,Apache旗下的HBase、Hive、Storm等框架都有在使用Disruptor。LMAX 創(chuàng)建Disruptor作為可靠消息架構(gòu)的一部分,并將它設(shè)計(jì)成一種在不同組件中共享數(shù)據(jù)非??斓姆椒?。Disruptor運(yùn)行大致流程入下圖:
圖中左側(cè)(Input Disruptor部分)可以看作多生產(chǎn)者單消費(fèi)者模式。外部多個(gè)線(xiàn)程作為多生產(chǎn)者并發(fā)請(qǐng)求業(yè)務(wù)邏輯處理器(Business Logic Processor),這些請(qǐng)求的信息經(jīng)過(guò)Receiver存放在粉紅色的圓環(huán)中,業(yè)務(wù)處理器則作為消費(fèi)者從圓環(huán)中取得數(shù)據(jù)進(jìn)行處理。右側(cè)(Output Disruptor部分)則可看作單生產(chǎn)者多消費(fèi)者模式。業(yè)務(wù)邏輯處理器作為單生產(chǎn)者,發(fā)布數(shù)據(jù)到粉紅色圓環(huán)中,Publisher作為多個(gè)消費(fèi)者接受業(yè)務(wù)邏輯處理器的結(jié)果。這里兩處地方的數(shù)據(jù)共享都是通過(guò)那個(gè)粉紅色的圓環(huán),它就是Disruptor的核心設(shè)計(jì)RingBuffer。
Disruptor特點(diǎn)
Disruptor對(duì)偽共享的處理
RingBuffer類(lèi)
RingBuffer類(lèi)(即上節(jié)中粉紅色的圓環(huán))的類(lèi)關(guān)系圖如下:
通過(guò)源碼分析,RingBuffer的父類(lèi),RingBufferFields采用數(shù)組來(lái)實(shí)現(xiàn)存放線(xiàn)程間的共享數(shù)據(jù)。下圖,第57行,entries數(shù)組。
前面分析過(guò)數(shù)組比鏈表、樹(shù)更具有緩存友好性,此處不做細(xì)表。不使用LinkedBlockingQueue隊(duì)列,是基于無(wú)鎖機(jī)制的考慮。詳細(xì)分析可參考,并發(fā)編程網(wǎng)的翻譯。這里我們主要分析RingBuffer的繼承關(guān)系中的填充,解決緩存?zhèn)喂蚕韱?wèn)題。如下圖:?
依據(jù)JVM對(duì)象繼承關(guān)系中父類(lèi)屬性與子類(lèi)屬性,內(nèi)存地址連續(xù)排列布局,RingBufferPad的protected long p1,p2,p3,p4,p5,p6,p7;作為緩存前置填充,RingBuffer中的protected long p1,p2,p3,p4,p5,p6,p7;作為緩存后置填充。這樣任意線(xiàn)程訪(fǎng)問(wèn)RingBuffer時(shí),RingBuffer放在父類(lèi)RingBufferFields的屬性,都是獨(dú)占一行Cache line不會(huì)產(chǎn)生偽共享問(wèn)題。如圖,RingBuffer的操作字段在RingBufferFields中,使用rbf標(biāo)識(shí):
按照一行緩存64字節(jié)計(jì)算,前后填充56字節(jié)(7個(gè)long),中間大于等于8字節(jié)的內(nèi)容都能獨(dú)占一行Cache line,此處rbf是大于8字節(jié)的。
Sequence類(lèi)
Sequence類(lèi)用來(lái)跟蹤RingBuffer和事件處理器的增長(zhǎng)步數(shù),支持多個(gè)并發(fā)操作包括CAS指令和寫(xiě)指令。同時(shí)使用了Padding方式來(lái)實(shí)現(xiàn),如下為其類(lèi)結(jié)構(gòu)圖及Padding的類(lèi)。
Sequence里在volatile long value前后放置了7個(gè)long padding,來(lái)解決偽共享的問(wèn)題。示意如圖,此處Value等于8字節(jié):
也許讀者應(yīng)該會(huì)認(rèn)為這里的圖示比上面RingBuffer的圖示更好理解,這里的操作屬性只有一個(gè)value,兩個(gè)圖相互結(jié)合就更能理解了。
Sequencer的實(shí)現(xiàn)
在RingBuffer構(gòu)造函數(shù)里面存在一個(gè)Sequencer接口,用來(lái)遍歷數(shù)據(jù),在生產(chǎn)者和消費(fèi)者之間傳遞數(shù)據(jù)。Sequencer有兩個(gè)實(shí)現(xiàn)類(lèi),單生產(chǎn)者模式的實(shí)現(xiàn)SingleProducerSequencer與多生產(chǎn)者模式的實(shí)現(xiàn)MultiProducerSequencer。它們的類(lèi)結(jié)構(gòu)如圖:
單生產(chǎn)者是在Cache line中使用padding方式實(shí)現(xiàn),源碼如下:
多生產(chǎn)者則是使用 sun.misc.Unsafe來(lái)實(shí)現(xiàn)的。如下圖:
總結(jié)與使用示例
可見(jiàn)padding方式在Disruptor中是處理偽共享常見(jiàn)的方式,JDK1.8的@Contended很好的解決了這個(gè)問(wèn)題,不知道Disruptor后面的版本是否會(huì)考慮使用它。
Disruptor使用示例代碼https://github.com/EasonFeng5870/disruptor_demo。
參考資料:
7個(gè)示例科普CPU Cache:http://coolshell.cn/articles/10249.html?
Linux Cache 機(jī)制:http://www.cnblogs.com/liloke/archive/2011/11/20/2255737.html?
《深入理解計(jì)算機(jī)系統(tǒng)》:第六章部分?
Disruptor官方文檔:https://github.com/LMAX-Exchange/disruptor/tree/master/docs?
Disruptor并發(fā)編程網(wǎng)文檔翻譯:http://ifeve.com/disruptor/
作者簡(jiǎn)介:
上海-周衛(wèi)理、北京-楊珍琪、北京-馮英圣、深圳-姜寄羽 傾力合作,另外感謝惠普系統(tǒng)架構(gòu)師吳治輝策劃支持。
from:?https://blog.csdn.net/qq_27680317/article/details/78486220?
總結(jié)
以上是生活随笔為你收集整理的一篇对伪共享、缓存行填充和CPU缓存讲的很透彻的文章的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 伪共享
- 下一篇: 伪共享(False Sharing)