緩存行、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就採用了 緩存行填充來提高程序性能

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章