天天看點

剖析Disruptor:為什麼會這麼快?(二)神奇的緩存行填充計算機入門緩存行解決方案-神奇的緩存行填充

作者:trisha  譯者:方騰飛 校對:丁一

(譯注:martin thompson很喜歡用mechanical sympathy這個短語,這個短語源于賽車駕駛,它反映了駕駛員對于汽車有一種天生的感覺,是以他們對于如何最佳的駕禦它非常有感覺。)

是以,對在學校學過的人是種複習,對未學過的人是個簡單介紹。但是請注意,這篇文章包含了大量的過度簡化。

cpu和主記憶體之間有好幾層緩存,因為即使直接通路主記憶體也是非常慢的。如果你正在多次對一塊資料做相同的運算,那麼在執行運算的時候把它加載到離cpu很近的地方就有意義了(比如一個循環計數-你不想每次循環都跑到主記憶體去取這個資料來增長它吧)。

剖析Disruptor:為什麼會這麼快?(二)神奇的緩存行填充計算機入門緩存行解決方案-神奇的緩存行填充

越靠近cpu的緩存越快也越小。是以l1緩存很小但很快(譯注:l1表示一級緩存),并且緊靠着在使用它的cpu核心。l2大一些,也慢一些,并且仍然隻能被一個單獨的 cpu 核使用。l3在現代多核機器中更普遍,仍然更大,更慢,并且被單個插槽上的所有 cpu 核共享。最後,你擁有一塊主存,由全部插槽上的所有 cpu 核共享。

當cpu執行運算的時候,它先去l1查找所需的資料,再去l2,然後是l3,最後如果這些緩存中都沒有,所需的資料就要去主記憶體拿。走得越遠,運算耗費的時間就越長。是以如果你在做一些很頻繁的事,你要確定資料在l1緩存中。

從cpu到

大約需要的 cpu 周期

大約需要的時間

主存

約60-80納秒

qpi 總線傳輸

(between sockets, not drawn)

約20ns

l3 cache

約40-45 cycles,

約15ns

l2 cache

約10 cycles,

約3ns

l1 cache

約3-4 cycles,

約1ns

寄存器

1 cycle

如果你的目标是讓端到端的延遲隻有 10毫秒,而其中花80納秒去主存拿一些未命中資料的過程将占很重的一塊。

現在需要注意一件有趣的事情,資料在緩存中不是以獨立的項來存儲的,如不是一個單獨的變量,也不是一個單獨的指針。緩存是由緩存行組成的,通常是64位元組(譯注:這篇文章發表時常用處理器的緩存行是64位元組的,比較舊的處理器緩存行是32位元組),并且它有效地引用主記憶體中的一塊位址。一個java的long類型是8位元組,是以在一個緩存行中可以存8個long類型的變量。

剖析Disruptor:為什麼會這麼快?(二)神奇的緩存行填充計算機入門緩存行解決方案-神奇的緩存行填充

(為了簡化,我将忽略多級緩存)

是以如果你資料結構中的項在記憶體中不是彼此相鄰的(連結清單,我正在關注你呢),你将得不到免費緩存加載所帶來的優勢。并且在這些資料結構中的每一個項都可能會出現緩存未命中。

不過,所有這種免費加載有一個弊端。設想你的long類型的資料不是數組的一部分。設想它隻是一個單獨的變量。讓我們稱它為<code>head</code>,這麼稱呼它其實沒有什麼原因。然後再設想在你的類中有另一個變量緊挨着它。讓我們直接稱它為<code>tail</code>。現在,當你加載<code>head</code>到緩存的時候,你也免費加載了<code>tail</code>。

剖析Disruptor:為什麼會這麼快?(二)神奇的緩存行填充計算機入門緩存行解決方案-神奇的緩存行填充

聽想來不錯。直到你意識到<code>tail</code>正在被你的生産者寫入,而<code>head</code>正在被你的消費者寫入。這兩個變量實際上并不是密切相關的,而事實上卻要被兩個不同核心中運作的線程所使用。

剖析Disruptor:為什麼會這麼快?(二)神奇的緩存行填充計算機入門緩存行解決方案-神奇的緩存行填充
剖析Disruptor:為什麼會這麼快?(二)神奇的緩存行填充計算機入門緩存行解決方案-神奇的緩存行填充

現在如果一些正在其他核心中運作的程序隻是想讀<code>tail</code>的值,整個緩存行需要從主記憶體重新讀取。那麼一個和你的消費者無關的線程讀一個和<code>head</code>無關的值,它被緩存未命中給拖慢了。

當然如果兩個獨立的線程同時寫兩個不同的值會更糟。因為每次線程對緩存行進行寫操作時,每個核心都要把另一個核心上的緩存塊無效掉并重新讀取裡面的資料。你基本上是遇到兩個線程之間的寫沖突了,盡管它們寫入的是不同的變量。

你會看到disruptor消除這個問題,至少對于緩存行大小是64位元組或更少的處理器架構來說是這樣的(譯注:有可能處理器的緩存行是128位元組,那麼使用64位元組填充還是會存在僞共享問題),通過增加補全來確定ring buffer的序列号不會和其他東西同時存在于一個緩存行中。

<a href="http://ifeve.com/disruptor-cacheline-padding/#viewsource">檢視源代碼</a>

<code>1</code>

<code>public</code> <code>long</code> <code>p1, p2, p3, p4, p5, p6, p7;</code><code>// cache line padding</code>

<code>2</code>

<code>    </code><code>private</code> <code>volatile</code> <code>long</code> <code>cursor = initial_cursor_value;</code>

<code>3</code>

<code>    </code><code>public</code> <code>long</code> <code>p8, p9, p10, p11, p12, p13, p14;</code><code>// cache line padding</code>

是以沒有僞共享,就沒有和其它任何變量的意外沖突,沒有不必要的緩存未命中。

在你的<code>entry</code>類中也值得這樣做,如果你有不同的消費者往不同的字段寫入,你需要確定各個字段間不會出現僞共享。