剖析Disruptor:為什么會(huì)這么快?(三)偽共享
緩存系統(tǒng)中是以緩存行(cache line)為單位存儲(chǔ)的。緩存行是2的整數(shù)冪個(gè)連續(xù)字節(jié),一般為32-256個(gè)字節(jié)。最常見(jiàn)的緩存行大小是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)偽共享。
為了讓可伸縮性與線程數(shù)呈線性關(guān)系,就必須確保不會(huì)有兩個(gè)線程往同一個(gè)變量或緩存行中寫。兩個(gè)線程寫同一個(gè)變量可以在代碼中發(fā)現(xiàn)。為了確定互相獨(dú)立的變量 是否共享了同一個(gè)緩存行,就需要了解內(nèi)存布局,或找個(gè)工具告訴我們。Intel VTune就是這樣一個(gè)分析工具。本文中我將解釋Java對(duì)象的內(nèi)存布局以及我們?cè)撊绾翁畛渚彺嫘幸员苊鈧喂蚕怼?/p>
圖1說(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)重。
Java內(nèi)存布局(Java Memory Layout)
對(duì)于HotSpot JVM,所有對(duì)象都有兩個(gè)字長(zhǎng)的對(duì)象頭。第一個(gè)字是由24位哈希碼和8位標(biāo)志位(如鎖的狀態(tài)或作為鎖對(duì)象)組成的Mark Word。第二個(gè)字是對(duì)象所屬類的引用。如果是數(shù)組對(duì)象還需要一個(gè)額外的字來(lái)存儲(chǔ)數(shù)組的長(zhǎng)度。每個(gè)對(duì)象的起始地址都對(duì)齊于8字節(jié)以提高性能。因此當(dāng)封裝對(duì) 象的時(shí)候?yàn)榱烁咝?,?duì)象字段聲明的順序會(huì)被重排序成下列基于字節(jié)大小的順序:
- doubles (8) 和 longs (8)
- ints (4) 和 floats (4)
- shorts (2) 和 chars (2)
- booleans (1) 和 bytes (1)
- references (4/8)
- <子類字段重復(fù)上述順序>
了解這些之后就可以在任意字段間用7個(gè)long來(lái)填充緩存行。在Disruptor里我們對(duì)RingBuffer的cursor和BatchEventProcessor的序列進(jìn)行了緩存行填充。
為了展示其性能影響,我們啟動(dòng)幾個(gè)線程,每個(gè)都更新它自己獨(dú)立的計(jì)數(shù)器。計(jì)數(shù)器是volatile long類型的,所以其它線程能看到它們的進(jìn)展。
- public final class FalseSharing
- implements Runnable
- {
- public final static int NUM_THREADS = 4; // change
- public final static long ITERATIONS = 500L * 1000L * 1000L;
- private final int arrayIndex;
- private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
- static
- {
- for (int i = 0; i < longs.length; i++)
- {
- longs[i] = new VolatileLong();
- }
- }
- public FalseSharing(final int arrayIndex)
- {
- this.arrayIndex = arrayIndex;
- }
- public static void main(final String[] args) throws Exception
- {
- final long start = System.nanoTime();
- runTest();
- System.out.println("duration = " + (System.nanoTime() - start));
- }
- private static void runTest() throws InterruptedException
- {
- Thread[] threads = new Thread[NUM_THREADS];
- for (int i = 0; i < threads.length; i++)
- {
- threads[i] = new Thread(new FalseSharing(i));
- }
- for (Thread t : threads)
- {
- t.start();
- }
- for (Thread t : threads)
- {
- t.join();
- }
- }
- public void run()
- {
- long i = ITERATIONS + 1;
- while (0 != --i)
- {
- longs[arrayIndex].value = i;
- }
- }
- public final static class VolatileLong
- {
- public volatile long value = 0L;
- public long p1, p2, p3, p4, p5, p6; // comment out
- }
- }
結(jié)果(Results)
運(yùn)行上面的代碼,增加線程數(shù)以及添加/移除緩存行的填充,下面的圖2描述了我得到的結(jié)果。這是在我4核Nehalem上測(cè)得的運(yùn)行時(shí)間。
從不斷上升的測(cè)試所需時(shí)間中能夠明顯看出偽共享的影響。沒(méi)有緩存行競(jìng)爭(zhēng)時(shí),我們幾近達(dá)到了隨著線程數(shù)的線性擴(kuò)展。
這并不是個(gè)完美的測(cè)試,因?yàn)槲覀儾荒艽_定這些VolatileLong會(huì)布局在內(nèi)存的什么位置。它們是獨(dú)立的對(duì)象。但是經(jīng)驗(yàn)告訴我們同一時(shí)間分配的對(duì)象趨向集中于一塊。
所以你也看到了,偽共享可能是無(wú)聲的性能殺手。