CPU高速緩存(Cache Memory)
CPU高速緩存
CPU緩存即高速緩沖存儲器,是位於CPU與主內存間的一種容量較小但速度很高的存儲器。由於CPU的速度遠高於主內存,CPU直接從內存中存取數據要等待一定時間周期,Cache中保存着CPU剛用過或循環使用的一部分數據,當CPU再次使用該部分數據時可從Cache中直接調用,減少CPU的等待時間,提高了系統的效率。
在CPU訪問存儲設備時,無論是存取數據抑或存取指令,都趨於聚集在一片連續的區域中,這就是局部性原理。
時間局部性(Temporal Locality):如果一個信息項正在被訪問,那么在近期它很可能還會被再次訪問。
比如循環、遞歸、方法的反復調用等。
空間局部性(Spatial Locality):如果一個存儲器的位置被引用,那么將來他附近的位置也會被引用。
比如順序執行的代碼、連續創建的兩個對象、數組等。
多CPU多核緩存架構
物理CPU:物理CPU就是插在主機上的真實的CPU硬件,在Linux下可以數不同的physical id 來確認主機的物理CPU個數。
核心數:我們常常會聽說多核處理器,其中的核指的就是核心數。在Linux下可以通過cores來確認主機的物理CPU的核心數。
邏輯CPU:邏輯CPU跟超線程技術有聯系,假如物理CPU不支持超線程的,那么邏輯CPU的數量等於核心數的數量;如果物理CPU支持超線程,那么邏輯CPU的數目是核心數數目的兩倍。在Linux下可以通過 processors 的數目來確認邏輯CPU的數量。
現代CPU為了提升執行效率,減少CPU與內存的交互,一般在CPU上集成了多級緩存架構,常見的為三級緩存結構。

緩存一致性(Cache coherence)
計算機體系結構中,緩存一致性是共享資源數據的一致性,這些數據最終存儲在多個本地緩存中。當系統中的客戶機維護公共內存資源的緩存時,可能會出現數據不一致的問題,這在多處理系統中的cpu中尤其如此。

在共享內存多處理器系統中,每個處理器都有一個單獨的緩存內存,共享數據可能有多個副本:一個副本在主內存中,一個副本在請求它的每個處理器的本地緩存中。當數據的一個副本發生更改時,其他副本必須反映該更改。緩存一致性是確保共享操作數(數據)值的變化能夠及時地在整個系統中傳播的規程。
緩存一致性的要求
寫傳播(Write Propagation)
對任何緩存中的數據的更改都必須傳播到對等緩存中的其他副本(該緩存行的副本)。
事務串行化(Transaction Serialization)
對單個內存位置的讀/寫必須被所有處理器以相同的順序看到。理論上,一致性可以在加載/存儲粒度上執行。然而,在實踐中,它通常在緩存塊的粒度上執行。
一致性機制(Coherence mechanisms)
確保一致性的兩種最常見的機制是
窺探機制(snooping )和基於目錄的機制(directory-based),這兩種機制各有優缺點。如果有足夠的帶寬可用,基於協議的窺探往往會更快,因為所有事務都是所有處理器看到的請求/響應。其缺點是窺探是不可擴展的。每個請求都必須廣播到系統中的所有節點,這意味着隨着系統變大,(邏輯或物理)總線的大小及其提供的帶寬也必須增加。另一方面,目錄往往有更長的延遲(3跳 請求/轉發/響應),但使用更少的帶寬,因為消息是點對點的,而不是廣播的。由於這個原因,許多較大的系統(>64處理器)使用這種類型的緩存一致性。
總線仲裁機制
在計算機中,數據通過總線在處理器和內存之間傳遞。每次處理器和內存之間的數據傳遞都是通過一系列步驟來完成的,這一系列步驟稱之為
總線事務(Bus Transaction)。總線事務包括讀事務(Read Transaction)和寫事務(WriteTransaction)。讀事務從內存傳送數據到處理器,寫事務從處理器傳送數據到內存,每個事務會讀/寫內存中一個或多個物理上連續的字。這里的關鍵是,
總線會同步試圖並發使用總線的事務。在一個處理器執行總線事務期間,總線會禁止其他的處理器和I/O設備執行內存的讀/寫。
假設處理器A,B和C同時向總線發起總線事務,這時
總線仲裁(Bus Arbitration)會對競爭做出裁決,這里假設總線在仲裁后判定處理器A在競爭中獲勝(總線仲裁會確保所有處理器都能公平的訪問內存)。此時處理器A繼續它的總線事務,而其他兩個處理器則要等待處理器A的總線事務完成后才能再次執行內存訪問。假設在處理器A執行總線事務期間(不管這個總線事務是讀事務還是寫事務),處理器D向總線發起了總線事務,此時處理器D的請求會被總線禁止。
總線的這種工作機制可以把所有處理器對內存的訪問以串行化的方式來執行。在任意時間點,最多只能有一個處理器可以訪問內存。這個特性確保了單個總線事務之中的內存讀/寫操作具有原子性。
原子操作是指不可被中斷的一個或者一組操作。
處理器會自動保證基本的內存操作的原子性,也就是一個處理器從內存中讀取或者寫入一個字節時,其他處理器是不能訪問這個字節的內存地址。最新的處理器能自動保證單處理器對同一個緩存行里進行16/32/64位的操作是原子的,但是復雜的內存操作處理器是不能自動保證其原子性的,比如跨總線寬度、跨多個緩存行和跨頁表的訪問。
處理器提供總線鎖定和緩存鎖定兩個機制來保證復雜內存操作的原子性。
總線鎖定
總線鎖定就是使用處理器提供的一個 LOCK#信號,當其中一個處理器在總線上輸出此信號時,其它處理器的請求將被阻塞住,那么該處理器可以獨占共享內存。
緩存鎖定
由於總線鎖定阻止了被阻塞處理器和所有內存之間的通信,而輸出LOCK#信號的CPU可能只需要鎖住特定的一塊內存區域,因此總線鎖定開銷較大。
緩存鎖定是指內存區域如果被緩存在處理器的緩存行中,並且在Lock操作期間被鎖定,那么當它執行鎖操作回寫到內存時,處理器不會在總線上聲言LOCK#信號(總線鎖定信號),而是修改內部的內存地址,並允許它的緩存一致性機制來保證操作的原子性,因為緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據,當其他處理器回寫已被鎖定的緩存行的數據時,會使緩存行無效。
緩存鎖定不能使用的特殊情況:
- 當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行時,則處理器會調用總線鎖定。
- 有些處理器不支持緩存鎖定。
《64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf》中有如下描述:
The 32-bit IA-32 processors support locked atomic operations on locations in system memory. These operations are typically used to manage shared data structures (such as semaphores, segment descriptors, system segments, or page tables) in which two or more processors may try simultaneously to modify the same field or flag. The processor uses three interdependent mechanisms for carrying out locked atomic operations:
• Guaranteed atomic operations
• Bus locking, using the LOCK# signal and the LOCK instruction prefix
• Cache coherency protocols that ensure that atomic operations can be carried out on cached data structures (cache lock); this mechanism is present in the Pentium 4, Intel Xeon, and P6 family processors
32位的IA-32處理器支持對系統內存中的位置進行鎖定的原子操作。這些操作通常用於管理共享的數據結構(如信號量、段描述符、系統段或頁表),在這些結構中,兩個或多個處理器可能同時試圖修改相同的字段或標志。處理器使用三種相互依賴的機制來執行鎖定的原子操作:
- 有保證的原子操作
- 總線鎖定,使用LOCK#信號和LOCK指令前綴
- 緩存一致性協議,確保原子操作可以在緩存的數據結構上執行(緩存鎖);這種機制出現在Pentium 4、Intel Xeon和P6系列處理器中
總線窺探(Bus Snooping)
總線窺探(Bus snooping)是緩存中的一致性控制器(snoopy cache)監視或窺探總線事務的一種方案,其目標是在分布式共享內存系統中維護緩存一致性。包含一致性控制器(snooper)的緩存稱為snoopy緩存。該方案由Ravishankar和Goodman於1983年提出。
工作原理
當特定數據被多個緩存共享時,處理器修改了共享數據的值,更改必須傳播到所有其他具有該數據副本的緩存中。這種更改傳播可以防止系統違反緩存一致性。數據變更的通知可以通過總線窺探來完成。所有的窺探者都在監視總線上的每一個事務。如果一個修改共享緩存塊的事務出現在總線上,所有的窺探者都會檢查他們的緩存是否有共享塊的相同副本。如果緩存中有共享塊的副本,則相應的窺探者執行一個動作以確保緩存一致性。
這個動作可以是刷新緩存塊或使緩存塊失效。它還涉及到緩存塊狀態的改變,這取決於緩存一致性協議(cache coherence protocol)。
窺探協議類型
根據管理寫操作的本地副本的方式,有兩種窺探協議:
Write-invalidate
當處理器寫入一個共享緩存塊時,其他緩存中的所有共享副本都會通過總線窺探失效。這種方法確保處理器只能讀寫一個數據的一個副本。其他緩存中的所有其他副本都無效。這是最常用的窺探協議。MSI、MESI、MOSI、MOESI和MESIF協議屬於該類型。
Write-update
當處理器寫入一個共享緩存塊時,其他緩存的所有共享副本都會通過總線窺探更新。這個方法將寫數據廣播到總線上的所有緩存中。它比write-invalidate協議引起更大的總線流量。這就是為什么這種方法不常見。Dragon和firefly協議屬於此類別。
一致性協議(Coherence protocol)
一致性協議在多處理器系統中應用於高速緩存一致性。為了保持一致性,人們設計了各種模型和協議,如MSI、MESI(又名Illinois)、MOSI、MOESI、MERSI、MESIF、write-once、Synapse、Berkeley、Firefly和Dragon協議。
- MSI protocol, the basic protocol from which the MESI protocol is derived.
- Write-once (cache coherency), an early form of the MESI protocol.
- MESI protocol
- MOSI protocol
- MOESI protocol
- MESIF protocol
- MERSI protocol
- Dragon protocol
- Firefly protocol
MESI協議
MESI協議
是一個基於寫失效的緩存一致性協議,是支持回寫(write-back)緩存的最常用協議。也稱作伊利諾伊協議 (Illinois protocol,因為是在伊利諾伊大學厄巴納-香檳分校被發明的)。與寫通過(write through)緩存相比,回寫緩沖能節約大量帶寬。總是有“臟”(dirty)狀態表示緩存中的數據與主存中不同。MESI協議要求在緩存不命中(miss)且數據塊在另一個緩存時,允許緩存到緩存的數據復制。與MSI協議相比,MESI協議減少了主存的事務數量。這極大改善了性能。
狀態
緩存行有4種不同的狀態:
已修改Modified (M)
緩存行是臟的(dirty),與主存的值不同。如果別的CPU內核要讀主存這塊數據,該緩存行必須回寫到主存,狀態變為共享(S).
獨占Exclusive (E)
緩存行只在當前緩存中,但是干凈的--緩存數據同於主存數據。當別的緩存讀取它時,狀態變為共享;當前寫數據時,變為已修改狀態。
共享Shared (S)
緩存行也存在於其它緩存中且是未修改的。緩存行可以在任意時刻拋棄。
無效Invalid (I)
緩存行是無效的
任意一對緩存,對應緩存行的相容關系:
當塊標記為 M (已修改), 在其他緩存中的數據副本被標記為I(無效).
偽共享的問題
如果多個核的線程在操作同一個緩存行中的不同變量數據,那么就會出現頻繁的緩存失效,即使在代碼層面看這兩個線程操作的數據之間完全沒有關系。這種不合理的資源競爭情況就是偽共享(False Sharing)。
linux下查看Cache Line大小
Cache Line大小是64Byte
或者執行 cat /proc/cpuinfo 命令

避免偽共享方案
1.緩存行填充
class Pointer { volatile long x; //避免偽共享: 緩存行填充 long p1, p2, p3, p4, p5, p6, p7; volatile long y;
2.使用 @sun.misc.Contended 注解(java8)
注意需要配置jvm參數:-XX:-RestrictContended
測試
1 public class FalseSharingTest { 2 3 public static void main(String[] args) throws InterruptedException { 4 testPointer(new Pointer()); 5 } 6 7 private static void testPointer(Pointer pointer) throws InterruptedException { 8 long start = System.currentTimeMillis(); 9 Thread t1 = new Thread(() -> { 10 for (int i = 0; i < 100000000; i++) { 11 pointer.x++; 12 } 13 }); 14 15 Thread t2 = new Thread(() -> { 16 for (int i = 0; i < 100000000; i++) { 17 pointer.y++; 18 } 19 }); 20 21 t1.start(); 22 t2.start(); 23 t1.join(); 24 t2.join(); 25 26 System.out.println(pointer.x+","+pointer.y); 27 28 System.out.println(System.currentTimeMillis() - start); 29 30 31 } 32 } 33 34 35 class Pointer { 36 // 避免偽共享: @Contended + jvm參數:-XX:-RestrictContended jdk8支持 37 //@Contended 38 volatile long x; 39 //避免偽共享: 緩存行填充 40 //long p1, p2, p3, p4, p5, p6, p7; 41 volatile long y;
