深刻淺出計算機組成原理學習筆記:第五十四講

1、引子

堅持到底就是勝利,終於咱們⼀起來到了專欄的最後一個主題。讓我一塊兒帶你來看一看,CPU到底能有多快。在接下來的兩講裏,我會帶你一塊兒來看一個開源項目Disruptor。
看看咱們怎麼利用CPU和高速緩存的硬件特性,來設計一個對於性能有極限追求的系統。java

不知道你還記不記得,在第37講裏,爲了優化4毫秒專用鋪設光纖的故事。實際上,最在乎極限性能的並非互聯網公司,而是高頻交易公司。咱們今天講解的Disruptor就是由一家專門
作高頻交易的公司LMAX開源出來的。算法

有意思的是,Disruptor的開發語言,並非不少人心目中最容易作到性能極限的C/C++,而是性能受限於JVM的Java。這究竟是怎麼意回事呢?那經過這意講,
你就能體會到,其實只要通曉硬件層面的原理,即便是像Java這樣的高級語言,也可以把CPU的性能發揮到極限。數組

2、PaddingCache Line,體驗高速緩存的威力

咱們先來看看Disruptor裏面一段神奇的代碼。這段代碼裏,Disruptor在RingBufferPad這個類裏面定義了p1,p2一直到p7這樣7個long類型的變量。緩存

abstract class RingBufferPad
{
    protected long p1, p2, p3, p4, p5, p6, p7;
}

一、緩存行填充

我在看到這段代碼的第一反應是,變量名取得不規範,p1-p7這樣的變量名沒有明確的意義啊。不過,當我深刻了解了Disruptor的設計和源代碼,才發現這些變量名取得恰如其分。由於這些變量就是沒有實際意
義,只是幫助咱們進行緩存行填充(Padding Cache Line),使得咱們可以儘量地用上CPU高速緩存(CPU Cache)。那麼緩存存填充這個黑科技究竟是什麼樣的呢?咱們接着往下看。bash

不知道你還記不記得,咱們在35講裏面的這個表格。若是訪問內置在CPU裏的L1 Cache或者L2 Cache,訪問延時是內存的1/15乃至1/100。而內存的訪問速度,實際上是遠遠慢於CPU的。
想要追求極限性能,須要咱們儘量地多從CPU Cache裏面拿數據,而不是從內存裏面拿數據。服務器

二、CPU從內存加載數據到CPU Cache裏面的時候,不是一個變量一個變量加載的,而是加載固定長度的CacheLine

CPU Cache裝載內存裏面的數據,不是一個一個字段加載的,而是加載一整個緩存存。舉個例子,若是咱們定義了一個長度爲64的long類型的數組。那麼數據從內存加載到CPU Cache裏面的時候,
不是一個一個數組元素加載的,而是一次性加載固定長度的一個緩存行。數據結構

咱們如今的64位Intel CPU的計算機,緩存行一般是64個字節(Bytes)。一個long類型的數據須要8個字節,因此咱們一會兒會加載8個long類型的數據。也就是說,一次加載數組裏面連續的8個數值。
這樣的加載方式使得咱們遍歷數組元素的時候會很快。由於後面連續7次的數據訪問都會命中緩存,不須要從新從內存裏面去讀取數據。這個性能層面的好處,我在第37講的第一個例子裏面爲你演示過,印象不深的話,能夠返回去看看。多線程

三、對於類裏面定義的單獨的變量,就不容易享受到CPU Cache紅利了

可是,在咱們不是使用數組,而是使用單獨的變量的時候,這裏就會出現問題了。在Disruptor的RingBuffer(環形緩衝區)的代碼裏面,定義了一個單獨的long類型的變量。
這個變量叫做INITIAL_CURSOR_VALUE,用來存放RingBuffer起始的元素位置。框架

CPU在加載數據的時候,天然也會把這個數據從內存加載到高速緩存裏面來。不過,這個時候,高速緩存裏面除了這個數據,還會加載這個數據先後定義的其餘變量。這個時候,問題就來了。
Disruptor是一個多線程的服務器框架,在這個數據先後定義的其餘變量,可能會被多個不一樣的線程去更新數據、讀取數據。這些性能

寫入以及讀取的請求,會來自於不一樣的CPU Core。因而,爲了保證數據的同步更新,咱們不得不把CPUCache裏面的數據,從新寫回到內存裏面去或者從新從內存裏面加載數據。

而咱們剛剛說過,這些CPU Cache的寫回和加載,都不是以一個變量做爲單位的。這些動做都是以整個Cache Line做爲單位的。因此,當INITIAL_CURSOR_VALUE先後的那些變量被寫回到內存的時候,
這個字段本身也寫回到了內存,這個常量的緩存也就失效了。當咱們要再次讀取這個值的時候,要再從新從內存讀取。這也就意味着,讀取速度大大變慢了。

......


abstract class RingBufferPad
{
    protected long p1, p2, p3, p4, p5, p6, p7;
}
	


abstract class RingBufferFields<E> extends RingBufferPad
{
    ......
}


public final class RingBuffer<E> extends RingBufferFields<E> implements Cursored, EventSequencer<E>, EventSink<E>
{
    public static final long INITIAL_CURSOR_VALUE = Sequence.INITIAL_VALUE;
    protected long p1, p2, p3, p4, p5, p6, p7;
    ......

面臨這樣一個狀況,Disruptor裏發明了一個神奇的代碼技巧,這個技巧就是緩存行填充。Disruptor在INITIAL_CURSOR_VALUE的先後,分別定義了7個long類型的變量。前面的7個來自繼承的RingBufferPad
類,後面的7個則是直接定義在RingBuffer類裏面。這14個變量沒有任何實際的用途。咱們既不會去讀他們,也不會去寫他們。

而INITIAL_CURSOR_VALUE又是一個常量,也不會進行修改。因此,一旦它被加載到CPU Cache以後,只要被頻繁地讀取訪問,就不會再被換出Cache了。這也就意味着,對於這個值的讀取速度,
會是一直是CPUCache的訪問速度,而不是內存的訪問速度。

3、使用RingBuffer,利用緩存和分支預測

其實這個利用CPU Cache的性能的思路,貫穿了整個Disruptor。Disruptor整個框架,其實就是一個高速的生產者-消費者模型(Producer-Consumer)下的隊列。
生產者不停地往隊列裏面生產新的須要處理的任務,而消費者不停地從隊列裏面處理掉這些任務。

一、要實現一個隊列,最合適的數據結構應該是鏈表

若是你熟悉算法和數據結構,那你應該很是清楚,若是要實現一個隊列,最合適的數據結構應該是鏈表。咱們只要維護好鏈表的頭和尾,就能很容易實現一個隊列。
生產者只要不斷地往鏈表的尾部不斷插入新的節點,而消費者只須要不斷從頭部取出最老的節點進行處理就行了。咱們能夠很容易實現生產者-消費者模型。實際上,

Java本身的基礎庫裏面就有LinkedBlockingQueue這樣的隊列庫,能夠直接用在生產者-消費者模式上。

二、Disruptor裏面並無用LinkedBlockingQueue,而是使用了一個RingBuffer這樣的數據結構

不過,Disruptor裏面並無用LinkedBlockingQueue,而是使用了一個RingBuffer這樣的數據結構,這個RingBuffer的底層實現則是一個固定長度的數組。比起鏈表形式的實現,
數組的數據在內存裏面會存在空間局部性。

就像上面咱們看到的,數組的連續多個元素會一併加載到CPU Cache裏面來,因此訪問遍歷的速度會更快。而鏈表裏面各個節點的數據,多半不會出如今相鄰的內存空間,
天然也就享受不到整個Cache Line加載後數據連續從高速緩存存裏被訪問到的優點。

除此以外,數據的遍歷訪問還有一個很大的優點,就是CPU層面的分支預測會很準確。這可使得咱們更有這一部分的原理若是你已經不太記得了,
能夠回過頭去複習一下第25講關於分支預測的內容。

4、總結延伸

好了,不知道講完這些,你有沒有體會到Disruptor這個框架的神奇之處呢?

CPU從內存加載數據到CPU Cache裏面的時候,不是一個變量一個變量加載的,而是加載固定長度的CacheLine。若是是加載數組裏面的數據,那麼CPU就會加載到數組裏面連續的多個數據。
因此,數組的遍歷很容易享受到CPU Cache那風馳電掣的速度帶來的紅利。

對於類裏面定義的單獨的變量,就不容易享受到CPU Cache紅利了。由於這些字段雖然在內存層面會分配到一塊兒,可是實際應用的時候每每沒有什麼關聯。因而,就會出現多個CPU Core訪問的狀況下,
數據頻繁在CPU Cache和內存裏面來來回回的狀況。而Disruptor很取巧地在須要頻繁高速訪問的常量

INITIAL_CURSOR_VALUE 先後,各定義了7個沒有任何做⽤和讀寫請求的long類型的變量。

這樣,不管在內存的什麼位置上,這個INITIAL_CURSOR_VALUE所在的CacheLine都不會有任何寫更新的請求。咱們就能夠始終在Cache Line裏面讀到它的值,而不須要從內存裏面去讀取數據,
也就大大加速了Disruptor的性能。

這樣的思路,其實滲透在Disruptor這個開源框架的方方面面。做爲一個生產者-消費者模型,Disruptor並無選擇使用鏈表來實現一個隊列,而是使用了RingBuffer。RingBuffer底層的數據結構則是一個固定長度的數組。這個數組不只讓咱們更容易用好CPU Cache,對CPU執行過程當中的分支預測也很是有利。更準確的分支預測,可使得咱們更好地利用好CPU的流水線,讓代碼跑得更快。

相關文章
相關標籤/搜索