深扒Disruptor高性能的原因
一,使用數(shù)組存儲(chǔ)
二,緩存行優(yōu)勢(shì)&偽共享
三,內(nèi)存屏障
參考文章:https://ifeve.com/dissecting-disruptor-whats-so-special/
一,使用數(shù)組存儲(chǔ)
之所以ringbuffer采用這種數(shù)據(jù)結(jié)構(gòu),是因?yàn)樗诳煽肯鬟f方面有很好的性能。這就夠了,不過(guò)它還有一些其他的優(yōu)點(diǎn)。
首先,因?yàn)樗菙?shù)組,所以要比鏈表快,而且有一個(gè)容易預(yù)測(cè)的訪(fǎng)問(wèn)模式。(譯者注:數(shù)組內(nèi)元素的內(nèi)存地址的連續(xù)性存儲(chǔ)的)。這是對(duì)CPU緩存友好的—也就是說(shuō),在硬件級(jí)別,數(shù)組中的元素是會(huì)被預(yù)加載的,因此在ringbuffer當(dāng)中,cpu無(wú)需時(shí)不時(shí)去主存加載數(shù)組中的下一個(gè)元素。(校對(duì)注:因?yàn)橹灰粋€(gè)元素被加載到緩存行,其他相鄰的幾個(gè)元素也會(huì)被加載進(jìn)同一個(gè)緩存行)
其次,你可以為數(shù)組預(yù)先分配內(nèi)存,使得數(shù)組對(duì)象一直存在(除非程序終止)。這就意味著不需要花大量的時(shí)間用于垃圾回收。此外,不像鏈表那樣,需要為每一個(gè)添加到其上面的對(duì)象創(chuàng)造節(jié)點(diǎn)對(duì)象—對(duì)應(yīng)的,當(dāng)刪除節(jié)點(diǎn)時(shí),需要執(zhí)行相應(yīng)的內(nèi)存清理操作。
在本文中并沒(méi)有介紹如何避免ringbuffer產(chǎn)生重疊,以及如何對(duì)ringbuffer進(jìn)行讀寫(xiě)操作。你可能注意到了我將ringbuffer和鏈表那樣的數(shù)據(jù)結(jié)構(gòu)進(jìn)行比較,因?yàn)槲也⒉徽J(rèn)為鏈表是實(shí)際問(wèn)題的標(biāo)準(zhǔn)答案。
當(dāng)你將Disruptor和基于 隊(duì)列之類(lèi)的實(shí)現(xiàn)進(jìn)行比較時(shí),事情將變得很有趣。隊(duì)列通常注重維護(hù)隊(duì)列的頭尾元素,添加和刪除元素等。所有的這些我都沒(méi)有在ringbuffer里提到,這是因?yàn)閞ingbuffer不負(fù)責(zé)這些事情,我們把這些操作都移到了數(shù)據(jù)結(jié)構(gòu)(ringbuffer)的外部。
二,緩存行優(yōu)勢(shì)&偽共享
計(jì)算機(jī)基礎(chǔ)
CPU是你機(jī)器的心臟,最終由它來(lái)執(zhí)行所有運(yùn)算和程序。主內(nèi)存(RAM)是你的數(shù)據(jù)(包括代碼行)存放的地方。本文將忽略硬件驅(qū)動(dòng)和網(wǎng)絡(luò)之類(lèi)的東西,因?yàn)镈isruptor的目標(biāo)是盡可能多的在內(nèi)存中運(yùn)行。
CPU和主內(nèi)存之間有好幾層緩存,因?yàn)榧词怪苯釉L(fǎng)問(wèn)主內(nèi)存也是非常慢的。如果你正在多次對(duì)一塊數(shù)據(jù)做相同的運(yùn)算,那么在執(zhí)行運(yùn)算的時(shí)候把它加載到離CPU很近的地方就有意義了(比如一個(gè)循環(huán)計(jì)數(shù)-你不想每次循環(huán)都跑到主內(nèi)存去取這個(gè)數(shù)據(jù)來(lái)增長(zhǎng)它吧)。
越靠近CPU的緩存越快也越小。所以L(fǎng)1緩存很小但很快(譯注:L1表示一級(jí)緩存),并且緊靠著在使用它的CPU內(nèi)核。L2大一些,也慢一些,并且仍然只能被一個(gè)單獨(dú)的 CPU 核使用。L3在現(xiàn)代多核機(jī)器中更普遍,仍然更大,更慢,并且被單個(gè)插槽上的所有CPU 核共享。最后,你擁有一塊主存,由全部插槽上的所有 CPU 核共享。
當(dāng)CPU執(zhí)行運(yùn)算的時(shí)候,它先去L1查找所需的數(shù)據(jù),再去L2,然后是L3,最后如果這些緩存中都沒(méi)有,所需的數(shù)據(jù)就要去主內(nèi)存拿。走得越遠(yuǎn),運(yùn)算耗費(fèi)的時(shí)間就越長(zhǎng)。所以如果你在做一些很頻繁的事,你要確保數(shù)據(jù)在L1緩存中。
Martin和Mike的QCon presentation演講中給出了一些緩存未命中的消耗數(shù)據(jù):
| 從CPU到 | 大約需要的 CPU 周期 | 大約需要的時(shí)間 |
| 主存 | 約60-80納秒 | |
| QPI 總線(xiàn)傳輸 (between sockets, not drawn) |
約20ns | |
| L3 cache | 約40-45 cycles, | 約15ns |
| L2 cache | 約10 cycles, | 約3ns |
| L1 cache | 約3-4 cycles, | 約1ns |
| 寄存器 | 1 cycle |
如果你的目標(biāo)是讓端到端的延遲只有 10毫秒,而其中花80納秒去主存拿一些未命中數(shù)據(jù)的過(guò)程將占很重的一塊。
緩存行
現(xiàn)在需要注意一件有趣的事情,數(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類(lèi)型是8字節(jié),因此在一個(gè)緩存行中可以存8個(gè)long類(lèi)型的變量。
(為了簡(jiǎn)化,我將忽略多級(jí)緩存)
非常奇妙的是如果你訪(fǎng)問(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類(lèi)型的數(shù)據(jù)不是數(shù)組的一部分。設(shè)想它只是一個(gè)單獨(dú)的變量。讓我們稱(chēng)它為head,這么稱(chēng)呼它其實(shí)沒(méi)有什么原因。然后再設(shè)想在你的類(lèi)中有另一個(gè)變量緊挨著它。讓我們直接稱(chēng)它為tail。現(xiàn)在,當(dāng)你加載head到緩存的時(shí)候,你也免費(fèi)加載了tail。
聽(tīng)想來(lái)不錯(cuò)。直到你意識(shí)到tail正在被你的生產(chǎn)者寫(xiě)入,而head正在被你的消費(fèi)者寫(xiě)入。這兩個(gè)變量實(shí)際上并不是密切相關(guān)的,而事實(shí)上卻要被兩個(gè)不同內(nèi)核中運(yùn)行的線(xiàn)程所使用。
設(shè)想你的消費(fèi)者更新了head的值。緩存中的值和內(nèi)存中的值都被更新了,而其他所有存儲(chǔ)head的緩存行都會(huì)都會(huì)失效,因?yàn)槠渌彺嬷?code>head不是最新值了。請(qǐng)記住我們必須以整個(gè)緩存行作為單位來(lái)處理(譯注:這是CPU的實(shí)現(xiàn)所規(guī)定的,詳細(xì)可參見(jiàn)深入分析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)的線(xiàn)程讀一個(gè)和head無(wú)關(guān)的值,它被緩存未命中給拖慢了。
當(dāng)然如果兩個(gè)獨(dú)立的線(xiàn)程同時(shí)寫(xiě)兩個(gè)不同的值會(huì)更糟。因?yàn)槊看尉€(xiàn)程對(duì)緩存行進(jìn)行寫(xiě)操作時(shí),每個(gè)內(nèi)核都要把另一個(gè)內(nèi)核上的緩存塊無(wú)效掉并重新讀取里面的數(shù)據(jù)。你基本上是遇到兩個(gè)線(xiàn)程之間的寫(xiě)沖突了,盡管它們寫(xiě)入的是不同的變量。
這叫作“偽共享”(譯注:可以理解為錯(cuò)誤的共享),因?yàn)槊看文阍L(fǎng)問(wèn)head你也會(huì)得到tail,而且每次你訪(fǎng)問(wèn)tail,你也會(huì)得到head。這一切都在后臺(tái)發(fā)生,并且沒(méi)有任何編譯警告會(huì)告訴你,你正在寫(xiě)一個(gè)并發(fā)訪(fǎng)問(wèn)效率很低的代碼。
解決方案-神奇的緩存行填充
你會(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è)緩存行中。
1 |
publiclongp1, p2, p3, p4, p5, p6, p7;// cache line padding |
2 |
privatevolatilelongcursor = INITIAL_CURSOR_VALUE; |
3 |
publiclongp8, p9, p10, p11, p12, p13, p14;// cache line padding |
因此沒(méi)有偽共享,就沒(méi)有和其它任何變量的意外沖突,沒(méi)有不必要的緩存未命中。
在你的Entry類(lèi)中也值得這樣做,如果你有不同的消費(fèi)者往不同的字段寫(xiě)入,你需要確保各個(gè)字段間不會(huì)出現(xiàn)偽共享。
修改:Martin寫(xiě)了一個(gè)從技術(shù)上來(lái)說(shuō)更準(zhǔn)確更詳細(xì)的關(guān)于偽共享的文章,并且發(fā)布了性能測(cè)試結(jié)果。
三,內(nèi)存屏障
什么是內(nèi)存屏障?
它是一個(gè)CPU指令。沒(méi)錯(cuò),又一次,我們?cè)谟懻揅PU級(jí)別的東西,以便獲得我們想要的性能(Martin著名的Mechanical Sympathy理論)。基本上,它是這樣一條指令: a)確保一些特定操作執(zhí)行的順序; b)影響一些數(shù)據(jù)的可見(jiàn)性(可能是某些指令執(zhí)行后的結(jié)果)。
編譯器和CPU可以在保證輸出結(jié)果一樣的情況下對(duì)指令重排序,使性能得到優(yōu)化。插入一個(gè)內(nèi)存屏障,相當(dāng)于告訴CPU和編譯器先于這個(gè)命令的必須先執(zhí)行,后于這個(gè)命令的必須后執(zhí)行。正如去拉斯維加斯旅途中各個(gè)站點(diǎn)的先后順序在你心中都一清二楚。
內(nèi)存屏障另一個(gè)作用是強(qiáng)制更新一次不同CPU的緩存。例如,一個(gè)寫(xiě)屏障會(huì)把這個(gè)屏障前寫(xiě)入的數(shù)據(jù)刷新到緩存,這樣任何試圖讀取該數(shù)據(jù)的線(xiàn)程將得到最新值,而不用考慮到底是被哪個(gè)cpu核心或者哪顆CPU執(zhí)行的。
和Java有什么關(guān)系?
現(xiàn)在我知道你在想什么——這不是匯編程序。它是Java。
這里有個(gè)神奇咒語(yǔ)叫volatile(我覺(jué)得這個(gè)詞在Java規(guī)范中從未被解釋清楚)。如果你的字段是volatile,Java內(nèi)存模型將在寫(xiě)操作后插入一個(gè)寫(xiě)屏障指令,在讀操作前插入一個(gè)讀屏障指令。
這意味著如果你對(duì)一個(gè)volatile字段進(jìn)行寫(xiě)操作,你必須知道:
1、一旦你完成寫(xiě)入,任何訪(fǎng)問(wèn)這個(gè)字段的線(xiàn)程將會(huì)得到最新的值。
2、在你寫(xiě)入前,會(huì)保證所有之前發(fā)生的事已經(jīng)發(fā)生,并且任何更新過(guò)的數(shù)據(jù)值也是可見(jiàn)的,因?yàn)閮?nèi)存屏障會(huì)把之前的寫(xiě)入值都刷新到緩存。
舉個(gè)例子唄!
很高興你這樣說(shuō)了。又是時(shí)候讓我來(lái)畫(huà)幾個(gè)甜甜圈了。
RingBuffer的指針(cursor)(譯注:指向隊(duì)尾元素)屬于一個(gè)神奇的volatile變量,同時(shí)也是我們能夠不用鎖操作就能實(shí)現(xiàn)Disruptor的原因之一。
生產(chǎn)者將會(huì)取得下一個(gè)Entry(或者是一批),并可對(duì)它(們)作任意改動(dòng), 把它(們)更新為任何想要的值。如你所知,在所有改動(dòng)都完成后,生產(chǎn)者對(duì)ring buffer調(diào)用commit方法來(lái)更新序列號(hào)(譯注:把cursor更新為該Entry的序列號(hào))。對(duì)volatile字段(cursor)的寫(xiě)操作創(chuàng)建了一個(gè)內(nèi)存屏障,這個(gè)屏障將刷新所有緩存里的值(或者至少相應(yīng)地使得緩存失效)。
這時(shí)候,消費(fèi)者們能獲得最新的序列號(hào)碼(8),并且因?yàn)閮?nèi)存屏障保證了它之前執(zhí)行的指令的順序,消費(fèi)者們可以確信生產(chǎn)者對(duì)7號(hào)Entry所作的改動(dòng)已經(jīng)可用。
…那么消費(fèi)者那邊會(huì)發(fā)生什么?
消費(fèi)者中的序列號(hào)是volatile類(lèi)型的,會(huì)被若干個(gè)外部對(duì)象讀取——其他的下游消費(fèi)者可能在跟蹤這個(gè)消費(fèi)者。ProducerBarrier/RingBuffer(取決于你看的是舊的還是新的代碼)跟蹤它以確保環(huán)沒(méi)有出現(xiàn)重疊(wrap)的情況(譯注:為了防止下游的消費(fèi)者和上游的消費(fèi)者對(duì)同一個(gè)Entry競(jìng)爭(zhēng)消費(fèi),導(dǎo)致在環(huán)形隊(duì)列中互相覆蓋數(shù)據(jù),下游消費(fèi)者要對(duì)上游消費(fèi)者的消費(fèi)情況進(jìn)行跟蹤)。
所以,如果你的下游消費(fèi)者(C2)看見(jiàn)前一個(gè)消費(fèi)者(C1)在消費(fèi)號(hào)碼為12的Entry,當(dāng)C2的讀取也到了12,它在更新序列號(hào)前將可以獲得C1對(duì)該Entry的所作的更新。
基本來(lái)說(shuō)就是,C1更新序列號(hào)前對(duì)ring buffer的所有操作(如上圖黑色所示),必須先發(fā)生,待C2拿到C1更新過(guò)的序列號(hào)之后,C2才可以為所欲為(如上圖藍(lán)色所示)。
對(duì)性能的影響
內(nèi)存屏障作為另一個(gè)CPU級(jí)的指令,沒(méi)有鎖那樣大的開(kāi)銷(xiāo)。內(nèi)核并沒(méi)有在多個(gè)線(xiàn)程間干涉和調(diào)度。但凡事都是有代價(jià)的。內(nèi)存屏障的確是有開(kāi)銷(xiāo)的——編譯器/cpu不能重排序指令,導(dǎo)致不可以盡可能地高效利用CPU,另外刷新緩存亦會(huì)有開(kāi)銷(xiāo)。所以不要以為用volatile代替鎖操作就一點(diǎn)事都沒(méi)。
你會(huì)注意到Disruptor的實(shí)現(xiàn)對(duì)序列號(hào)的讀寫(xiě)頻率盡量降到最低。對(duì)volatile字段的每次讀或?qū)懚际窍鄬?duì)高成本的操作。但是,也應(yīng)該認(rèn)識(shí)到在批量的情況下可以獲得很好的表現(xiàn)。如果你知道不應(yīng)對(duì)序列號(hào)頻繁讀寫(xiě),那么很合理的想到,先獲得一整批Entries,并在更新序列號(hào)前處理它們。這個(gè)技巧對(duì)生產(chǎn)者和消費(fèi)者都適用。以下的例子來(lái)自BatchConsumer:
01 |
longnextSequence = sequence +1; |
02 |
while(running) |
03 |
{ |
04 |
try |
05 |
{ |
06 |
finallongavailableSequence = consumerBarrier.waitFor(nextSequence); |
07 |
while(nextSequence <= availableSequence) |
08 |
{ |
09 |
entry = consumerBarrier.getEntry(nextSequence); |
10 |
handler.onAvailable(entry); |
11 |
nextSequence++; |
12 |
} |
13 |
handler.onEndOfBatch(); |
14 |
sequence = entry.getSequence(); |
15 |
} |
16 |
… |
17 |
catch(finalException ex) |
18 |
{ |
19 |
exceptionHandler.handle(ex, entry); |
20 |
sequence = entry.getSequence(); |
21 |
nextSequence = entry.getSequence() +1; |
22 |
} |
23 |
} |
(你會(huì)注意到,這是個(gè)舊式的代碼和命名習(xí)慣,因?yàn)檫@是摘自我以前的博客文章,我認(rèn)為如果直接轉(zhuǎn)換為新式的代碼和命名習(xí)慣會(huì)讓人有點(diǎn)混亂)
在上面的代碼中,我們?cè)谙M(fèi)者處理entries的循環(huán)中用一個(gè)局部變量(nextSequence)來(lái)遞增。這表明我們想盡可能地減少對(duì)volatile類(lèi)型的序列號(hào)的進(jìn)行讀寫(xiě)。
總結(jié)
內(nèi)存屏障是CPU指令,它允許你對(duì)數(shù)據(jù)什么時(shí)候?qū)ζ渌M(jìn)程可見(jiàn)作出假設(shè)。在Java里,你使用volatile關(guān)鍵字來(lái)實(shí)現(xiàn)內(nèi)存屏障。使用volatile意味著你不用被迫選擇加鎖,并且還能讓你獲得性能的提升。
但是,你需要對(duì)你的設(shè)計(jì)進(jìn)行一些更細(xì)致的思考,特別是你對(duì)volatile字段的使用有多頻繁,以及對(duì)它們的讀寫(xiě)有多頻繁。
總結(jié)
以上是生活随笔為你收集整理的深扒Disruptor高性能的原因的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: python核心数据类型_Python核
- 下一篇: python oracle连接池_【Py