重慶網(wǎng)站建設(shè)排名武漢seo首頁
Disruptor 的開發(fā)語言,并不是很多人心目中最容易做到性能極限的 C/C++,而是性能受限于 JVM 的 Java。其實只要通曉硬件層面的原理,即使是像 Java 這樣的高級語言,也能夠把 CPU 的性能發(fā)揮到極限。
一、Padding Cache Line,體驗高速緩存的威力
Disruptor 里面一段神奇的代碼。這段代碼里,Disruptor 在 RingBufferPad 這個類里面定義了 p1,p2 一直到 p7 這樣 7 個 long 類型的變量。這些變量沒有實際意義,只是幫助我們進(jìn)行緩存行填充(Padding Cache Line),使得我們能夠盡可能地用上 CPU 高速緩存(CPU Cache)。
abstract class RingBufferPad
{protected long p1, p2, p3, p4, p5, p6, p7;
}
CPU Cache 裝載內(nèi)存里面的數(shù)據(jù),不是一個一個字段加載的,而是加載一整個緩存行。舉個例子,如果我們定義了一個長度為 64 的 long 類型的數(shù)組。那么數(shù)據(jù)從內(nèi)存加載到 CPU Cache 里面的時候,會一次性加載固定長度的一個緩存行。
我們現(xiàn)在的 64 位 Intel CPU 的計算機,緩存行通常是 64 個字節(jié)(Bytes)。一個 long 類型的數(shù)據(jù)需要 8 個字節(jié),所以一下子會加載 8 個 long 類型的數(shù)據(jù)。也就是說,一次加載數(shù)組里面連續(xù)的 8 個數(shù)值。這樣的加載方式可以加快遍歷數(shù)組元素時的速度。因為后面連續(xù) 7 次的數(shù)據(jù)訪問都會命中緩存,不需要重新從內(nèi)存里面去讀取數(shù)據(jù)。
但是,不使用數(shù)組,而是使用單獨的變量的時候,這里就會出現(xiàn)問題了。在 Disruptor 的 RingBuffer(環(huán)形緩沖區(qū))的代碼里面,定義了一個 RingBufferFields 類,里面有 indexMask 和其他幾個變量,用來存放 RingBuffer 的內(nèi)部狀態(tài)信息。
CPU 在加載數(shù)據(jù)的時候,自然也會把這個數(shù)據(jù)從內(nèi)存加載到高速緩存里面來。不過,這個時候,高速緩存里面除了這個數(shù)據(jù),還會加載這個數(shù)據(jù)前后定義的其他變量。問題就來了,Disruptor 是一個多線程的服務(wù)器框架,在這個數(shù)據(jù)前后定義的其他變量,可能會被多個不同的線程去更新數(shù)據(jù)、讀取數(shù)據(jù)。這些寫入以及讀取的請求,會來自于不同的 CPU Core。于是,為了保證數(shù)據(jù)的同步更新,我們不得不把 CPU Cache 里面的數(shù)據(jù),重新寫回到內(nèi)存里面去或者重新從內(nèi)存里面加載數(shù)據(jù)。
CPU Cache 的寫回和加載,都不是以一個變量作為單位的。這些動作都是以整個 Cache Line 作為單位的。所以,當(dāng) INITIAL_CURSOR_VALUE 前后的那些變量被寫回到內(nèi)存的時候,這個字段自己也寫回到了內(nèi)存,這個常量的緩存也就失效了。要再次讀取這個值的時候,還需重新從內(nèi)存讀取。這也就意味著,讀取速度大大變慢了。
面臨這樣一個情況,Disruptor 里有一種神奇的代碼技巧,就是緩存行填充。Disruptor 在 RingBufferFields 里面定義的變量的前后,分別定義了 7 個 long 類型的變量。前面的 7 個來自繼承的 RingBufferPad 類,后面的 7 個則是直接定義在 RingBuffer 類里面。這 14 個變量沒有任何實際的用途。我們既不會去讀他們,也不會去寫他們。
......abstract class RingBufferPad
{protected long p1, p2, p3, p4, p5, p6, p7;
}abstract class RingBufferFields<E> extends RingBufferPad
{...... private final long indexMask;private final Object[] entries;protected final int bufferSize;protected final Sequencer sequencer;......
}public final class RingBuffer<E> extends RingBufferFields<E> implements Cursored, EventSequencer<E>, EventSink<E>
{...... protected long p1, p2, p3, p4, p5, p6, p7;......
}
?RingBufferFields 里面定義的這些變量都是 final 的,第一次寫入之后不會再進(jìn)行修改。所以,一旦它被加載到 CPU Cache 之后,只要被頻繁地讀取訪問,就不會再被換出 Cache 了。這也就意味著,對于這個值的讀取速度,會是一直是 CPU Cache 的訪問速度,而不是內(nèi)存的訪問速度。
二、使用 RingBuffer,利用緩存和分支預(yù)測
利用 CPU Cache 性能的思路,貫穿了整個 Disruptor。Disruptor 整個框架,其實就是一個高速的生產(chǎn)者 - 消費者模型(Producer-Consumer)下的隊列。生產(chǎn)者不停地往隊列里面生產(chǎn)新的需要處理的任務(wù),而消費者不停地從隊列里面處理掉這些任務(wù)。
實現(xiàn)一個隊列,最合適的數(shù)據(jù)結(jié)構(gòu)應(yīng)該是鏈表。只要維護好鏈表的頭和尾,就能很容易實現(xiàn)一個隊列。生產(chǎn)者只要不斷地往鏈表的尾部不斷插入新的節(jié)點,而消費者只需要不斷從頭部取出最老的節(jié)點進(jìn)行處理就好了。實際上,Java 自己的基礎(chǔ)庫里面就有 LinkedBlockingQueue 這樣的隊列庫,可以直接用在生產(chǎn)者 - 消費者模式上。
不過,Disruptor 里面并沒有用 LinkedBlockingQueue,而是使用了一個 RingBuffer 這樣的數(shù)據(jù)結(jié)構(gòu),這個 RingBuffer 的底層實現(xiàn)是一個固定長度的數(shù)組。比起鏈表形式的實現(xiàn),數(shù)組的數(shù)據(jù)在內(nèi)存里面會存在空間局部性。
數(shù)組的連續(xù)多個元素會一并加載到 CPU Cache 里面來,所以訪問遍歷的速度會更快。而鏈表里面各個節(jié)點的數(shù)據(jù),多半不會出現(xiàn)在相鄰的內(nèi)存空間,自然也就享受不到整個 Cache Line 加載后數(shù)據(jù)連續(xù)從高速緩存里面被訪問到的優(yōu)勢。
除此之外,數(shù)據(jù)的遍歷訪問還有一個很大的優(yōu)勢,就是 CPU 層面的分支預(yù)測會很準(zhǔn)確。可以更有效地利用?CPU 里面的多級流水線,使程序就會跑得更快。
三、無鎖的 RingBuffer
(一)緩慢的鎖
Disruptor 作為一個高性能的生產(chǎn)者 - 消費者隊列系統(tǒng),一個核心的設(shè)計就是通過 RingBuffer 實現(xiàn)一個無鎖隊列。
Java 里面的基礎(chǔ)庫里像?LinkedBlockingQueue 這樣的隊列庫比起 Disruptor 里用的 RingBuffer 要慢上很多。慢的第一個原因我們說過,因為鏈表的數(shù)據(jù)在內(nèi)存里面的布局對于高速緩存并不友好,而 RingBuffer 所使用的數(shù)組則不然。
另外一個重要的因素是?LinkedBlockingQueue 對鎖的依賴較強。在生產(chǎn)者 - 消費者模式里,我們可能有多個消費者,同樣也可能有多個生產(chǎn)者。多個生產(chǎn)者都要往隊列的尾指針里面添加新的任務(wù),就會產(chǎn)生多個線程的競爭。于是,在做這個事情的時候,生產(chǎn)者就需要拿到對于隊列尾部的鎖。同樣地,在多個消費者去消費隊列頭的時候,也就產(chǎn)生競爭。同樣消費者也要拿到鎖。
就算只有一個生產(chǎn)者,一個消費者,也是需要鎖的。一般來說,在生產(chǎn)者 - 消費者模式下,消費者要比生產(chǎn)者快。不然的話,隊列會產(chǎn)生積壓,隊列里面的任務(wù)會越堆越多。任務(wù)不能及時完成,內(nèi)存也會放不下。雖然生產(chǎn)者 - 消費者模型下,有一個隊列來作為緩沖區(qū),但是大部分情況下,這個緩沖區(qū)里面是空的。也就是說,即使只有一個生產(chǎn)者和一個消費者者,這個生產(chǎn)者指向的隊列尾和消費者指向的隊列頭是同一個節(jié)點。因此,生產(chǎn)者和消費者之間一樣會產(chǎn)生鎖競爭。
在 LinkedBlockingQueue 上,這個鎖機制是通過 Java 基礎(chǔ)庫 ReentrantLock?來實現(xiàn)的。這個鎖是一個用 Java 在 JVM 上直接實現(xiàn)的加鎖機制,鎖機制需要由 JVM 來進(jìn)行裁決。鎖的爭奪,會把沒有拿到鎖的線程掛起等待,也就需要經(jīng)過一次上下文切換(Context Switch)。
上下文切換的過程,需要把當(dāng)前執(zhí)行線程的寄存器等的信息,保存到線程棧里面。而這個過程也必然意味著,已經(jīng)加載到高速緩存里面的指令或者數(shù)據(jù),又回到了主內(nèi)存里面,會進(jìn)一步拖慢性能。
加鎖和不加鎖自增到5億性能對比:
package com.xuwenhao.perf.jmm;import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class LockBenchmark{public static void runIncrement(){long counter = 0;long max = 500000000L;long start = System.currentTimeMillis();while (counter < max) {counter++;}long end = System.currentTimeMillis();System.out.println("Time spent is " + (end-start) + "ms without lock");}public static void runIncrementWithLock(){Lock lock = new ReentrantLock();long counter = 0;long max = 500000000L;long start = System.currentTimeMillis();while (counter < max) {if (lock.tryLock()){counter++;lock.unlock();}}long end = System.currentTimeMillis();System.out.println("Time spent is " + (end-start) + "ms with lock");}public static void main(String[] args) {runIncrement();runIncrementWithLock();
Time spent is 207ms without lock
Time spent is 9603ms with lock
(二)無鎖的 RingBuffer
加鎖很慢,所以 Disruptor 的解決方案就是“無鎖”。這個“無鎖”指的是沒有操作系統(tǒng)層面的鎖。實際上,Disruptor 還是利用了一個 CPU 硬件支持的指令,稱之為 CAS(Compare And Swap,比較和交換)。在 Intel CPU 里面,這個對應(yīng)的指令就是 cmpxchg。
Disruptor 的 RingBuffer 是這么設(shè)計的:和直接在鏈表的頭和尾加鎖不同,Disruptor 的 RingBuffer 創(chuàng)建了一個 Sequence 對象,用來指向當(dāng)前的 RingBuffer 的頭和尾。這個頭和尾的標(biāo)識不是通過指針來實現(xiàn)的,而是通過一個序號。
在這個 RingBuffer 當(dāng)中,進(jìn)行生產(chǎn)者和消費者之間的資源協(xié)調(diào),采用的是對比序號的方式。當(dāng)生產(chǎn)者想要往隊列里加入新數(shù)據(jù)的時候,它會把當(dāng)前的生產(chǎn)者的 Sequence 的序號,加上需要加入的新數(shù)據(jù)的數(shù)量,然后和實際的消費者所在的位置進(jìn)行對比,看看隊列里是不是有足夠的空間加入這些數(shù)據(jù),而不會覆蓋掉消費者還沒有處理完的數(shù)據(jù)。
在 Sequence 的代碼里面,就是通過 compareAndSet 這個方法,并且最終調(diào)用到了 UNSAFE.compareAndSwapLong,也就是直接使用了 CAS 指令。
public boolean compareAndSet(final long expectedValue, final long newValue){return UNSAFE.compareAndSwapLong(this, VALUE_OFFSET, expectedValue, newValue);}public long addAndGet(final long increment){long currentValue;long newValue;do{currentValue = get();newValue = currentValue + increment;}while (!compareAndSet(currentValue, newValue));return newValue;
這個 CAS 指令,也就是比較和交換的操作,并不是基礎(chǔ)庫里的一個函數(shù)。它也不是操作系統(tǒng)里面實現(xiàn)的一個系統(tǒng)調(diào)用,而是一個 CPU 硬件支持的機器指令。在我們服務(wù)器所使用的 Intel CPU 上,就是 cmpxchg 這個指令。
compxchg [ax] (隱式參數(shù),EAX累加器), [bx] (源操作數(shù)地址), [cx] (目標(biāo)操作數(shù)地址)
cmpxchg 指令,一共有三個操作數(shù),第一個操作數(shù)不在指令里面出現(xiàn),是一個隱式的操作數(shù),也就是 EAX 累加寄存器里面的值。第二個操作數(shù)就是源操作數(shù),并且指令會對比這個操作數(shù)和上面的累加寄存器里面的值。如果值是相同的,那一方面,CPU 會把 ZF(也就是條件碼寄存器里面零標(biāo)志位的值)設(shè)置為 1,然后再把第三個操作數(shù)(也就是目標(biāo)操作數(shù)),設(shè)置到源操作數(shù)的地址上。如果不相等的話,就會把源操作數(shù)里面的值,設(shè)置到累加器寄存器里面。
單個指令是原子的,這也就意味著在使用 CAS 操作的時候,不再需要單獨進(jìn)行加鎖,直接調(diào)用就可以了。沒有了鎖,CPU 這部高速跑車就像在賽道上行駛,不會遇到需要上下文切換這樣的紅燈而停下來。雖然會遇到像 CAS 這樣復(fù)雜的機器指令,就好像賽道上會有 U 型彎一樣,不過不用完全停下來等待,?CPU 運行起來仍然會快很多。
那么,CAS 操作到底會有多快呢?我們還是用一段 Java 代碼來看一下。
package com.xuwenhao.perf.jmm;import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class LockBenchmark {public static void runIncrementAtomic(){AtomicLong counter = new AtomicLong(0);long max = 500000000L;long start = System.currentTimeMillis();while (counter.incrementAndGet() < max) {}long end = System.currentTimeMillis();System.out.println("Time spent is " + (end-start) + "ms with cas");}public static void main(String[] args) {runIncrementAtomic();}
?和上面的 counter 自增一樣,只不過這一次,自增采用了 AtomicLong 這個 Java 類。里面的 incrementAndGet 最終到了 CPU 指令層面,在實現(xiàn)的時候用的就是 CAS 操作??梢钥吹?#xff0c;它所花費的時間,雖然要比沒有任何鎖的操作慢上一個數(shù)量級,但是比起使用 ReentrantLock 這樣的操作系統(tǒng)鎖的機制,還是減少了一半以上的時間。
當(dāng)想要追求最極致的性能的時候,我們會從應(yīng)用層、貫穿到操作系統(tǒng),乃至最后的 CPU 硬件,搞清楚從高級語言到系統(tǒng)調(diào)用,乃至最后的匯編指令,這整個過程是怎么執(zhí)行代碼的。而這個,也是學(xué)習(xí)組成原理的意義所在。
【推薦閱讀】Disruptor 官方文檔,里面不僅包含了怎么用好 Disruptor,也包含了整個 Disruptor 框架的設(shè)計思路,是一份很好的閱讀學(xué)習(xí)材料。另外,Disruptor 的官方文檔里,還有很多文章、演講,詳細(xì)介紹了這個框架,很值得深入去看一看。Disruptor 的源代碼其實并不復(fù)雜,很適合用來學(xué)習(xí)怎么閱讀開源框架代碼。
課程鏈接:深入淺出計算機組成原理_組成原理_計算機基礎(chǔ)-極客時間?