CPU緩存 ? ??
? 執(zhí)行程序是靠運(yùn)行CPU執(zhí)行主存中代碼,但是CPU和主存的速度差異是非常大的,為了降低這種差距,在架構(gòu)中使用了CPU緩存救鲤,現(xiàn)在的計(jì)算機(jī)架構(gòu)中普遍使用了緩存,分為一級(jí)緩存秩冈,二級(jí)緩存本缠,還有一些具備三級(jí)緩存,我們可以看看這些組件的數(shù)據(jù)獲取訪問(wèn)速度入问。
從CPU到大約需要的 CPU 周期大約需要的時(shí)間
主存?約60-80納秒
QPI 總線傳輸
(between sockets, not drawn)
?約20ns
L3 cache約40-45 cycles,約15ns
L2 cache約10 cycles,約3ns
L1 cache約3-4 cycles,約1ns
寄存器1 cycle
如果要了解緩存搓茬,就必須要了解緩存的結(jié)構(gòu),以及多個(gè)CPU核心訪問(wèn)緩存存在的一些問(wèn)題和注意事項(xiàng)队他。
每個(gè)緩存里面都是由緩存行組成的卷仑,緩存系統(tǒng)中是以緩存行(cache line)為單位存儲(chǔ)的。緩存行是2的整數(shù)冪個(gè)連續(xù)字節(jié)麸折,一般為32-256個(gè)字節(jié)锡凝。最常見的緩存行大小是64個(gè)字節(jié)。當(dāng)多線程修改互相獨(dú)立的變量時(shí)垢啼,如果這些變量共享同一個(gè)緩存行窜锯,就會(huì)無(wú)意中影響彼此的性能,這就是偽共享芭析。緩存行上的寫競(jìng)爭(zhēng)是運(yùn)行在SMP系統(tǒng)中并行線程實(shí)現(xiàn)可伸縮性最重要的限制因素锚扎。有人將偽共享描述成無(wú)聲的性能殺手,因?yàn)閺拇a中很難看清楚是否會(huì)出現(xiàn)偽共享馁启。
偽共享問(wèn)題
圖中說(shuō)明了偽共享的問(wèn)題驾孔。在核心1上運(yùn)行的線程想更新變量X,同時(shí)核心2上的線程想要更新變量Y惯疙。不幸的是翠勉,這兩個(gè)變量在同一個(gè)緩存行中。每個(gè)線程都要去競(jìng)爭(zhēng)緩存行的所有權(quán)來(lái)更新變量霉颠。如果核心1獲得了所有權(quán)对碌,緩存子系統(tǒng)將會(huì)使核心2中對(duì)應(yīng)的緩存行失效。當(dāng)核心2獲得了所有權(quán)然后執(zhí)行更新操作蒿偎,核心1就要使自己對(duì)應(yīng)的緩存行失效朽们。這會(huì)來(lái)來(lái)回回的經(jīng)過(guò)L3緩存,大大影響了性能诉位。如果互相競(jìng)爭(zhēng)的核心位于不同的插槽骑脱,就要額外橫跨插槽連接,問(wèn)題可能更加嚴(yán)重不从。
緩存行帶來(lái)的鎖競(jìng)爭(zhēng)
處理器為了提高處理速度惜姐,不直接和內(nèi)存進(jìn)行通訊,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進(jìn)行操作,但操作完之后不知道何時(shí)會(huì)寫到內(nèi)存歹袁;如果對(duì)聲明了Volatile變量進(jìn)行寫操作坷衍,JVM就會(huì)向處理器發(fā)送一條Lock前綴的指令,將這個(gè)變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存条舔。但是就算寫回到內(nèi)存枫耳,如果其他處理器緩存的值還是舊的,再執(zhí)行計(jì)算操作就會(huì)有問(wèn)題孟抗,所以在多處理器下迁杨,為了保證各個(gè)處理器的緩存是一致的,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議凄硼,每個(gè)處理器通過(guò)嗅探在總線上傳播的數(shù)據(jù)來(lái)檢查自己緩存的值是不是過(guò)期了铅协,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無(wú)效狀態(tài)摊沉,當(dāng)處理器要對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候狐史,會(huì)強(qiáng)制重新從系統(tǒng)內(nèi)存里把數(shù)據(jù)讀到處理器緩存里。
? ? 當(dāng)多個(gè)線程對(duì)同一個(gè)緩存行訪問(wèn)時(shí)说墨,其中一個(gè)線程會(huì)鎖住緩存行骏全,然后操作,這時(shí)候其他線程沒辦法操作緩存行尼斧。
緩存行
需要注意姜贡,數(shù)據(jù)在緩存中不是以獨(dú)立的項(xiàng)來(lái)存儲(chǔ)的,如不是一個(gè)單獨(dú)的變量棺棵,也不是一個(gè)單獨(dú)的指針楼咳。緩存是由緩存行組成的,通常是64字節(jié)(譯注:這篇文章發(fā)表時(shí)常用處理器的緩存行是64字節(jié)的律秃,比較舊的處理器緩存行是32字節(jié))爬橡,并且它有效地引用主內(nèi)存中的一塊地址。一個(gè)Java的long類型是8字節(jié)棒动,因此在一個(gè)緩存行中可以存8個(gè)long類型的變量。
如果你訪問(wèn)一個(gè)long數(shù)組宾添,當(dāng)數(shù)組中的一個(gè)值被加載到緩存中船惨,它會(huì)額外加載另外7個(gè)。因此你能非陈粕拢快地遍歷這個(gè)數(shù)組粱锐。事實(shí)上,你可以非晨敢兀快速的遍歷在連續(xù)的內(nèi)存塊中分配的任意數(shù)據(jù)結(jié)構(gòu)怜浅。我在第一篇關(guān)于ring buffer的文章中順便提到過(guò)這個(gè),它解釋了我們的ring buffer使用數(shù)組的原因。
因此如果你數(shù)據(jù)結(jié)構(gòu)中的項(xiàng)在內(nèi)存中不是彼此相鄰的(鏈表恶座,我正在關(guān)注你呢)搀暑,你將得不到免費(fèi)緩存加載所帶來(lái)的優(yōu)勢(shì)。并且在這些數(shù)據(jù)結(jié)構(gòu)中的每一個(gè)項(xiàng)都可能會(huì)出現(xiàn)緩存未命中跨琳。
不過(guò)自点,所有這種免費(fèi)加載有一個(gè)弊端。設(shè)想你的long類型的數(shù)據(jù)不是數(shù)組的一部分脉让。設(shè)想它只是一個(gè)單獨(dú)的變量桂敛。讓我們稱它為head,這么稱呼它其實(shí)沒有什么原因溅潜。然后再設(shè)想在你的類中有另一個(gè)變量緊挨著它术唬。讓我們直接稱它為tail。現(xiàn)在滚澜,當(dāng)你加載head到緩存的時(shí)候粗仓,你也免費(fèi)加載了tail。
直到你意識(shí)到tail正在被你的生產(chǎn)者寫入博秫,而head正在被你的消費(fèi)者寫入潦牛。這兩個(gè)變量實(shí)際上并不是密切相關(guān)的,而事實(shí)上卻要被兩個(gè)不同內(nèi)核中運(yùn)行的線程所使用挡育。
設(shè)想你的消費(fèi)者更新了head的值巴碗。緩存中的值和內(nèi)存中的值都被更新了,而其他所有存儲(chǔ)head的緩存行都會(huì)都會(huì)失效即寒,因?yàn)槠渌彺嬷衕ead不是最新值了橡淆。請(qǐng)記住我們必須以整個(gè)緩存行作為單位來(lái)處理(譯注:這是CPU的實(shí)現(xiàn)所規(guī)定的,詳細(xì)可參見深入分析Volatile的實(shí)現(xiàn)原理)母赵,不能只把head標(biāo)記為無(wú)效逸爵。
現(xiàn)在如果一些正在其他內(nèi)核中運(yùn)行的進(jìn)程只是想讀tail的值,整個(gè)緩存行需要從主內(nèi)存重新讀取凹嘲。那么一個(gè)和你的消費(fèi)者無(wú)關(guān)的線程讀一個(gè)和head無(wú)關(guān)的值师倔,它被緩存未命中給拖慢了。
當(dāng)然如果兩個(gè)獨(dú)立的線程同時(shí)寫兩個(gè)不同的值會(huì)更糟周蹭。因?yàn)槊看尉€程對(duì)緩存行進(jìn)行寫操作時(shí)趋艘,每個(gè)內(nèi)核都要把另一個(gè)內(nèi)核上的緩存塊無(wú)效掉并重新讀取里面的數(shù)據(jù)。你基本上是遇到兩個(gè)線程之間的寫沖突了凶朗,盡管它們寫入的是不同的變量瓷胧。
這叫作“偽共享”(譯注:可以理解為錯(cuò)誤的共享),因?yàn)槊看文阍L問(wèn)head你也會(huì)得到tail棚愤,而且每次你訪問(wèn)tail搓萧,你也會(huì)得到head。這一切都在后臺(tái)發(fā)生,并且沒有任何編譯警告會(huì)告訴你瘸洛,你正在寫一個(gè)并發(fā)訪問(wèn)效率很低的代碼揍移。
避免偽共享
? 在Java中
? ? ? ? 你會(huì)看到Disruptor消除這個(gè)問(wèn)題,至少對(duì)于緩存行大小是64字節(jié)或更少的處理器架構(gòu)來(lái)說(shuō)是這樣的(譯注:有可能處理器的緩存行是128字節(jié)货矮,那么使用64字節(jié)填充還是會(huì)存在偽共享問(wèn)題),通過(guò)增加補(bǔ)全來(lái)確保ring buffer的序列號(hào)不會(huì)和其他東西同時(shí)存在于一個(gè)緩存行中羊精。
因此沒有偽共享,就沒有和其它任何變量的意外沖突囚玫,沒有不必要的緩存未命中喧锦。
?Java8實(shí)現(xiàn)字節(jié)填充避免偽共享?
? JVM參數(shù)??-XX:-RestrictContended?
? ?@Contended 位于 sun.misc 用于注解java 屬性字段,自動(dòng)填充字節(jié)抓督,防止偽共享
? 在C語(yǔ)言中
? ?避免偽共享燃少,編譯器會(huì)自動(dòng)將結(jié)構(gòu)體,字節(jié)補(bǔ)全和對(duì)其铃在,對(duì)其的大小最好是緩存行的長(zhǎng)度阵具。
? ?總的來(lái)說(shuō),結(jié)構(gòu)體實(shí)例會(huì)和它的最寬成員一樣對(duì)齊定铜。編譯器這樣做因?yàn)檫@是保證所有成員自對(duì)齊以獲得快速存取的最容易方法阳液。
從上面的情況可以看出,在設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)的時(shí)候揣炕,應(yīng)該盡量將只讀數(shù)據(jù)與讀寫數(shù)據(jù)分開帘皿,并具盡量將同一時(shí)間訪問(wèn)的數(shù)據(jù)組合在一起。這樣?CPU?能一次將需要的數(shù)據(jù)讀入畸陡。如:
?這樣的數(shù)據(jù)結(jié)構(gòu)就很不利鹰溜。
?在?X86?下,可以試著修改和調(diào)整它
CACHE_LINE_SIZE?– sizeof(int)+sizeof(name)*sizeof(name[0])%CACHE_LINE_SIZE看起來(lái)很不和諧丁恭,CACHE_LINE_SIZE表示高速緩存行為 64Bytes?大小曹动。?__align?用于顯式對(duì)齊。這種方式是使得結(jié)構(gòu)體字節(jié)對(duì)齊的大小為緩存行的大小