1. 程式人生 > >偽共享

偽共享

一、偽共享的定義:

偽共享的非標準定義為:快取系統中是以快取行(cache line)為單位儲存的,當多執行緒修改互相獨立的變數時,如果這些變數共享同一個快取行,就會無意中影響彼此的效能,這就是偽共享。

二、CPU快取機制

CPU 快取的百度百科定義為:

CPU 快取(Cache Memory)是位於 CPU 與記憶體之間的臨時儲存器,它的容量比記憶體小的多但是交換速度卻比記憶體要快得多。
快取記憶體的出現主要是為了解決 CPU 運算速度與記憶體讀寫速度不匹配的矛盾,因為 CPU 運算速度要比記憶體讀寫速度快很多,這樣會使 CPU 花費很長時間等待資料到來或把資料寫入記憶體。
在快取中的資料是記憶體中的一小部分,但這一小部分是短時間內 CPU 即將訪問的,當 CPU 呼叫大量資料時,就可避開記憶體直接從快取中呼叫,從而加快讀取速度。
CPU 和主記憶體之間有好幾層快取,因為即使直接訪問主記憶體也是非常慢的。如果你正在多次對一塊資料做相同的運算,那麼在執行運算的時候把它載入到離 CPU 很近的地方就有意義了。

按照資料讀取順序和與 CPU 結合的緊密程度,CPU 快取可以分為一級快取,二級快取,部分高階 CPU 還具有三級快取。每一級快取中所儲存的全部資料都是下一級快取的一部分,越靠近 CPU 的快取越快也越小。所以 L1 快取很小但很快(譯註:L1 表示一級快取),並且緊靠著在使用它的 CPU 核心。L2 大一些,也慢一些,並且仍然只能被一個單獨的 CPU 核使用。L3 在現代多核機器中更普遍,仍然更大,更慢,並且被單個插槽上的所有 CPU 核共享。最後,你擁有一塊主存,由全部插槽上的所有 CPU 核共享。擁有三級快取的的 CPU,到三級快取時能夠達到 95% 的命中率,只有不到 5% 的資料需要從記憶體中查詢。

多核機器的儲存結構如下圖所示:

 

當 CPU 執行運算的時候,它先去 L1 查詢所需的資料,再去 L2,然後是 L3,最後如果這些快取中都沒有,所需的資料就要去主記憶體拿。走得越遠,運算耗費的時間就越長。所以如果你在做一些很頻繁的事,你要確保資料在 L1 快取中。

Martin Thompson 給出了一些快取未命中的消耗資料,如下所示:

 

三、快取行

快取系統中是以快取行(cache line)為單位儲存的。快取行通常是 64 位元組(譯註:本文基於 64 位元組,其他長度的如 32 位元組等不適本文討論的重點),並且它有效地引用主記憶體中的一塊地址。例如一個 的 long 型別是 8 位元組,因此在一個快取行中可以存 8 個 long 型別的變數。所以,如果你訪問一個 long 陣列,當陣列中的一個值被載入到快取中,它會額外載入另外 7 個,以致你能非常快地遍歷這個陣列。事實上,你可以非常快速的遍歷在連續的記憶體塊中分配的任意資料結構。而如果你在資料結構中的項在記憶體中不是彼此相鄰的(如連結串列),你將得不到免費快取載入所帶來的優勢,並且在這些資料結構中的每一個項都可能會出現快取未命中。

如果存在這樣的場景,有多個執行緒操作不同的成員變數,但是相同的快取行,這個時候會發生什麼?。沒錯,偽共享(False Sharing)問題就發生了!有張 Disruptor 專案的經典示例圖,如下:

 

 

上圖中,一個執行在處理器 core1上的執行緒想要更新變數 X 的值,同時另外一個執行在處理器 core2 上的執行緒想要更新變數 Y 的值。但是,這兩個頻繁改動的變數都處於同一條快取行。兩個執行緒就會輪番傳送 RFO 訊息,佔得此快取行的擁有權。當 core1 取得了擁有權開始更新 X,則 core2 對應的快取行需要設為 I 狀態。當 core2 取得了擁有權開始更新 Y,則 core1 對應的快取行需要設為 I 狀態(失效態)。輪番奪取擁有權不但帶來大量的 RFO 訊息,而且如果某個執行緒需要讀此行資料時,L1 和 L2 快取上都是失效資料,只有 L3 快取上是同步好的資料。從前一篇我們知道,讀 L3 的資料非常影響效能。更壞的情況是跨槽讀取,L3 都要 miss,只能從記憶體上載入。

表面上 X 和 Y 都是被獨立執行緒操作的,而且兩操作之間也沒有任何關係。只不過它們共享了一個快取行,但所有競爭衝突都是來源於共享。

因此,當兩個以上CPU都要訪問同一個快取行大小的記憶體區域時,就會引起衝突,這種情況就叫“共享”。但是,這種情況裡面又包含了“其實不是共享”的“偽共享”情況。比如,兩個處理器各要訪問一個word,這兩個word卻存在於同一個cache line大小的區域裡,這時,從應用邏輯層面說,這兩個處理器並沒有共享記憶體,因為他們訪問的是不同的內容(不同的word)。但是因為cache line的存在和限制,這兩個CPU要訪問這兩個不同的word時,卻一定要訪問同一個cache line塊,產生了事實上的“共享”。顯然,由於cache line大小限制帶來的這種“偽共享”是我們不想要的,會浪費系統資源。

四、如何避免偽共享? 

1)讓不同執行緒操作的物件處於不同的快取行。

可以進行快取行填充(Padding) 。例如,如果一條快取行有 64 位元組,而 Java 程式的物件頭固定佔 8 位元組(32位系統)或 12 位元組( 64 位系統預設開啟壓縮, 不開壓縮為 16 位元組),所以我們只需要填 6 個無用的長整型補上6*8=48位元組,讓不同的 VolatileLong 物件處於不同的快取行,就避免了偽共享( 64 位系統超過快取行的 64 位元組也無所謂,只要保證不同執行緒不操作同一快取行就可以)。

2)使用編譯指示,強制使每一個變數對齊。

強制使物件按照快取行的邊界對齊。例如可以使資料按64位對齊,那麼一個快取行只有一個可操作物件,這樣發生偽共享之後,也只是對應快取行的資料變化,並不影響其他的物件。

 

 參考:

https://www.cnblogs.com/cyfonly/p/5800758.html

https://www.cnblogs.com/RunForLove/p/5624390.html