1. 程式人生 > >快取行、cpu偽共享和快取行填充

快取行、cpu偽共享和快取行填充

由於在看disruptor時瞭解到快取行,以及快取行填充的問題,所以各處瞭解記在這裡

一、快取行

CPU 為了更快的執行程式碼。於是當從記憶體中讀取資料時,並不是只讀自己想要的部分。而是讀取足夠的位元組來填入快取記憶體行。根據不同的 CPU ,快取記憶體行大小不同。如 X86 是 32BYTES ,而 ALPHA 是 64BYTES 。並且始終在第 32 個位元組或第 64 個位元組處對齊。這樣,當 CPU 訪問相鄰的資料時,就不必每次都從記憶體中讀取,提高了速度。 因為訪問記憶體要比訪問快取記憶體用的時間多得多。
這個快取是CPU內部自己的快取,內部的快取單位是行,叫做快取行。在多核環境下會出現CPU之間的記憶體同步問題(比如一個核載入了一份快取,另外一個核也要用到同一份資料),如果每個核每次需要時都往記憶體中存取(一個在讀快取,一個在寫快取時,造成資料不一致),這會帶來比較大的效能損耗,這個問題一般是通過MESI協議來解決的。
這裡寫圖片描述

圖1說明了偽共享的問題。在核心1上執行的執行緒想更新變數X,同時核心2上的執行緒想要更新變數Y。不幸的是,這兩個變數在同一個快取行中。每個執行緒都要去競爭快取行的所有權來更新變數。如果核心1獲得了所有權,快取子系統將會使核心2中對應的快取行失效。當核心2獲得了所有權然後執行更新操作,核心1就要使自己對應的快取行失效。這會來來回回的經過L3快取,大大影響了效能。如果互相競爭的核心位於不同的插槽,就要額外橫跨插槽連線,問題可能更加嚴重。

1.Cache的寫策略:

1)Write through(寫通)
     每次CPU修改了cache中的內容,Cache立即更新記憶體的內容
2) Write back(寫回)
    核心修改cache的內容後,cache並不會立即更新記憶體中的內容,而是等到這個cache line因為某種原因需要從cache中移除時,cache才會更新記憶體中的內容。

Write through(寫通)由於有大量的訪問記憶體的操作,效率太低,大多數處理器都使用Writeback(寫回)策略。
這裡寫圖片描述
Cache如何知道這行有沒有被修改?需要一個標誌-dirty標誌。Dirty標誌為1,表示cache的內容被修改,和記憶體的內容不一致,當該cache line被移除時,資料需要被更新到記憶體,dirty標誌位0(稱為clean),表示cache的內容和記憶體的內容一致。

2.Cache一致性

1)一致性問題的產生-資訊不對稱導致的問題
在多核處理器中,記憶體中有一個數據x,值為3,被快取到core0和core1中,如果core0將x修改為5,而core1
不知道x被修改,還在使用舊資料,就會導致程式出錯,這就是cache的不一致。
2)Cache一致性的底層操作
為了保證cache的一致性,處理器提供了兩個保證cache一致性的底層操作:Writeinvalidate和Write update。
這裡寫圖片描述

Write invalidate(置無效):當一個核心修改了一份資料,其他核心上如果有這份資料的複製,就置成無效。

這裡寫圖片描述

Write update(寫更新):當一個核心修改了一份資料,其他地方如果有這份資料的複製,就都更新到最新值。

Write invalidate是一種簡單的方式,不需要更新資料,如果core1和core2以後不再使用變數x,這時候採用write invalidate就非常有效,不過由於一個valid標誌對應一個Cache line,將valid標誌置成invalid後,這個cache line中其他的有效的資料也不能使用了。Write upodate策略會產生大量的資料更新操作,不過只用更新修改的資料,如果core1和core2會使用變數x,那麼writeupdate就比較有效。由於Writeinvalidate簡單,大多數處理器都是用Writeinvalidate策略。

MESI協議中包含M、E、S、I四個狀態,分別的意思是:

  • M(Modified)位。M 位為1 時表示當前Cache 行中包含的資料與儲存器中的資料不一致,而且它僅在本CPU的Cache 中有效,不在其他CPU的Cache
    中存在拷貝,在這個Cache行的資料是當前處理器系統中最新的資料拷貝。當CPU對這個Cache行進行替換操作時,必然會引發系統匯流排的寫週期,將Cache行中資料與記憶體中的資料同步。
  • E(Exclusive 獨佔)位。E 位為1 時表示當前Cache行中包含的資料有效,而且該資料僅在當前CPU的Cache中有效,而不在其他CPU的Cache中存在拷貝。在該Cache行中的資料是當前處理器系統中最新的資料拷貝,而且與儲存器中的資料一致。
  • S(Shared 共享)位。S 位為1 表示Cache行中包含的資料有效,而且在當前CPU和至少在其他一個CPU中具有副本。在該Cache行中的資料是當前處理器系統中最新的資料拷貝,而且與儲存器中的資料一致。
  • I(Invalid 無效)位。I 位為1 表示當前Cache 行中沒有有效資料或者該Cache行沒有使能。MESI協議在進行Cache行替換時,將優先使用I位為1的Cache行。
    這裡寫圖片描述
    這裡寫圖片描述
    這裡寫圖片描述

MESI協議狀態遷移圖:
這裡寫圖片描述

Local Read表示本核心讀本Cache的值,Local Write表示本核心寫Cache中的值,Remote Read表示其他核心
Remote Read讀其他Cache中的值,Remote write 表示其他核心寫其他Cache的值,箭頭表示本Cache line狀態的遷移

二、CPU偽共享

cpu在對快取行進行了不同的操作後,在cpu快取行中會記錄快取的不同狀態。當一個核要對共享的資料進行寫操作時,需要給其他核發送RFO(REQUEST FOR OWNER)訊息並把其他核的資料改成I態。這是一種比較消耗效能的操作。
cpu的偽共享問題本質是:幾個在邏輯上並不包含在同一個記憶體單元內的資料,由於被cpu載入在同一個快取行當中,當在多執行緒環境下,被不同的cpu執行,導致快取行失效而引起的大量的快取命中率降低。
例如:當兩個執行緒分別對一個數組中的兩份資料進行寫操作,每個執行緒操作不同index上的資料,看上去,兩份資料之間是不存在同步問題的,但是,由於他們可能在同一個cpu快取行當中,這就會使這一份快取行出現大量的快取失效,如前所述當一份執行緒更新時要給另一份執行緒傳送RFO訊息並把它的快取失效掉。

三、CacheLine補齊(快取行填充)

解決偽共享問題的一個辦法是讓每一份資料佔據一個快取行:因為快取行的大小是64個位元組,那我們只要讓陣列中每份資料的大小大於64個位元組,就可以保證他們在不同的快取行當中,就能避免這樣的偽共享問題。
比如一個類當中原本只有一個long型別的屬性。這樣這個型別的物件只佔了16個位元組(java物件頭有8位元組),如果這個型別被定義成一個長度為4的陣列,這個陣列的所有資料都可能在一個快取行當中,就可能出現偽共享問題,那麼這個時候,就可以採用補齊(padding)的辦法,在這個型別中加上public long a,b,c,d,e,f,g;這六個無用的屬性定義,使得這個型別的一個例項佔用記憶體達到64位元組,這樣這個型別的偽共享問題就得到了解決,在多執行緒當中對這個型別的陣列進行寫操作就能避免偽共享問題。

在Java 8中,可以採用@Contended在類級別上的註釋,來進行快取行填充。這樣,多執行緒情況下的偽共享衝突問題。 感興趣的同學可以檢視該文。
其實,@Contended註釋還可以應用於欄位級別(Field-Level),當應用於欄位級別時,被註釋的欄位將和其他欄位隔離開來,會被載入在獨立的快取行上。在欄位級別上,@Contended還支援一個“contention group”屬性(Class-Level不支援),同一個group的欄位們在記憶體上將是連續,但和其他欄位隔離開來。
執行時,必須加上虛擬機器引數-XX:-RestrictContended,@Contended註釋才會生效。

disruptor就採用了 快取行填充來提高程式效能