我在上一篇文章中提到RingBuffer後,咱們收到一些關於RingBuffer中填充高速緩存行的評論和疑問。因爲這個適合用漂亮的圖片來講明,因此我想這是下一個我該解決的問題了。編程
咱們常常提到一個短語Mechanical Sympathy,這個短語也是Martin博客的標題(譯註:Martin Thompson),Mechanical Sympathy講的是底層硬件是如何運做的,以及與其協做而非相悖的編程方式。(註解:sympathy這裏是共鳴、一致的含義)數組
我在上一篇文章中提到RingBuffer後,咱們收到一些關於RingBuffer中填充高速緩存行的評論和疑問。因爲這個適合用漂亮的圖片來講明,因此我想這是下一個我該解決的問題了。緩存
(譯註:Martin Thompson很喜歡用Mechanical Sympathy這個短語,這個短語源於×××駕駛,它反映了駕駛員對於汽車有一種天生的感受,因此他們對於如何最佳的駕御它很是有感受。)網絡
我喜歡在LMAX工做的緣由之一是,在這裏工做讓我明白從大學和A Level Computing所學的東西實際上仍是有意義的。作爲一個開發者你能夠逃避不去了解CPU,數據結構或者大O符號 —— 而我用了10年的職業生涯來忘記這些東西。可是如今看來,若是你知道這些知識並應用它,你能寫出一些很是巧妙和很是快速的代碼。數據結構
所以,對在學校學過的人是種複習,對未學過的人是個簡單介紹。可是請注意,這篇文章包含了大量的過分簡化。架構
CPU是你機器的心臟,最終由它來執行全部運算和程序。主內存(RAM)是你的數據(包括代碼行)存放的地方。本文將忽略硬件驅動和網絡之類的東西,由於Disruptor的目標是儘量多的在內存中運行。併發
CPU和主內存之間有好幾層緩存,由於即便直接訪問主內存也是很是慢的。若是你正在屢次對一塊數據作相同的運算,那麼在執行運算的時候把它加載到離CPU很近的地方就有意義了(好比一個循環計數-你不想每次循環都跑到主內存去取這個數據來增加它吧)。ide
越靠近CPU的緩存越快也越小。因此L1緩存很小但很快(譯註:L1表示一級緩存),而且緊靠着在使用它的CPU內核。L2大一些,也慢一些,而且 仍然只能被一個單獨的 CPU 核使用。L3在現代多核機器中更廣泛,仍然更大,更慢,而且被單個插槽上的全部 CPU 核共享。最後,你擁有一塊主存,由所有插槽上的全部 CPU 核共享。性能
當CPU執行運算的時候,它先去L1查找所需的數據,再去L2,而後是L3,最後若是這些緩存中都沒有,所需的數據就要去主內存拿。走得越遠,運算耗費的時間就越長。因此若是你在作一些很頻繁的事,你要確保數據在L1緩存中。測試
Martin和Mike的 QCon presentation演講中給出了一些緩存未命中的消耗數據:
若是你的目標是讓端到端的延遲只有 10毫秒,而其中花80納秒去主存拿一些未命中數據的過程將佔很重的一塊。
如今須要注意一件有趣的事情,數據在緩存中不是以獨立的項來存儲的,如不是一個單獨的變量,也不是一個單獨的指針。緩存是由緩存行組成的,一般是 64字節(譯註:這篇文章發表時經常使用處理器的緩存行是64字節的,比較舊的處理器緩存行是32字節),而且它有效地引用主內存中的一塊地址。一個Java 的long類型是8字節,所以在一個緩存行中能夠存8個long類型的變量。
(爲了簡化,我將忽略多級緩存)
很是奇妙的是若是你訪問一個long數組,當數組中的一個值被加載到緩存中,它會額外加載另外7個。所以你能很是快地遍歷這個數組。事實上,你能夠很是快速的遍歷在連續的內存塊中分配的任意數據結構。我在第一篇關於ring buffer的文章中順便提到過這個,它解釋了咱們的ring buffer使用數組的緣由。
所以若是你數據結構中的項在內存中不是彼此相鄰的(鏈表,我正在關注你呢),你將得不到免費緩存加載所帶來的優點。而且在這些數據結構中的每個項均可能會出現緩存未命中。
不過,全部這種免費加載有一個弊端。設想你的long類型的數據不是數組的一部分。設想它只是一個單獨的變量。讓咱們稱它爲head
,這麼稱呼它其實沒有什麼緣由。而後再設想在你的類中有另外一個變量緊挨着它。讓咱們直接稱它爲tail
。如今,當你加載head
到緩存的時候,你也免費加載了tail
。
聽想來不錯。直到你意識到tail
正在被你的生產者寫入,而head
正在被你的消費者寫入。這兩個變量實際上並非密切相關的,而事實上卻要被兩個不一樣內核中運行的線程所使用。
設想你的消費者更新了head
的值。緩存中的值和內存中的值都被更新了,而其餘全部存儲head
的緩存行都會都會失效,由於其它緩存中head
不是最新值了。請記住咱們必須以整個緩存行做爲單位來處理(譯註:這是CPU的實現所規定的,詳細可參見深刻分析Volatile的實現原理),不能只把head
標記爲無效。
如今若是一些正在其餘內核中運行的進程只是想讀tail
的值,整個緩存行須要從主內存從新讀取。那麼一個和你的消費者無關的線程讀一個和head
無關的值,它被緩存未命中給拖慢了。
固然若是兩個獨立的線程同時寫兩個不一樣的值會更糟。由於每次線程對緩存行進行寫操做時,每一個內核都要把另外一個內核上的緩存塊無效掉並從新讀取裏面的數據。你基本上是遇到兩個線程之間的寫衝突了,儘管它們寫入的是不一樣的變量。
這叫做「僞共享」(譯註:能夠理解爲錯誤的共享),由於每次你訪問head
你也會獲得tail
,並且每次你訪問tail
,你也會獲得head
。這一切都在後臺發生,而且沒有任何編譯警告會告訴你,你正在寫一個併發訪問效率很低的代碼。
你會看到Disruptor消除這個問題,至少對於緩存行大小是64字節或更少的處理器架構來講是這樣的(譯註:有可能處理器的緩存行是128字 節,那麼使用64字節填充仍是會存在僞共享問題),經過增長補全來確保ring buffer的序列號不會和其餘東西同時存在於一個緩存行中。
public long p1, p2, p3, p4, p5, p6, p7; // cache line padding private volatile long cursor = INITIAL_CURSOR_VALUE; public long p8, p9, p10, p11, p12, p13, p14; // cache line padding
所以沒有僞共享,就沒有和其它任何變量的意外衝突,沒有沒必要要的緩存未命中。
在你的Entry
類中也值得這樣作,若是你有不一樣的消費者往不一樣的字段寫入,你須要確保各個字段間不會出現僞共享。
修改:Martin寫了一個從技術上來講更準確更詳細的關於僞共享的文章,而且發佈了性能測試結果。
原文連接:http://ifeve.com/disruptor-padding/
譯文連接:http://ifeve.com/disruptor-cacheline-padding/
轉載地址 :http://developer.51cto.com/art/201306/399082.htm