存儲器的層次結構:
存儲器是分層次的,離CPU越近的存儲器,速度越快,每字節的成本越高,同時容量也越小。寄存器速度最快,離CPU最近,成本最高,所以個數容量有限,其次是高速緩存(緩存也是分級,有L1,L2,L3等緩存),再次是主存(普通內存),然後是本地磁盤,最次是遠程文件存儲。
緩存 | Register | L1 cache | L2 cache | L3 cache | Main Memory | 硬盤 |
---|---|---|---|---|---|---|
訪問時間 | < 1ns | 約1ns | 約3ns | 約15ns | 約80ns | 約2ms |
典型容量 | 幾十~幾百B | 幾十~幾百KB | 幾百KB | 幾百KB~幾MB | 幾GB | 幾百GB~幾TB |
Cache Line:
高速緩存其實就是一組稱之爲cache line的固定大小的數據塊,其大小是以突發讀或者突發寫週期的大小爲基礎的。即使處理器只存取一個字節的存儲器,高速緩存控制器也啓動整個存取器訪問週期並請求整個數據塊。緩存行第一個字節的地址總是突發週期尺寸的倍數。緩存行的起始位置總是與突發週期的開頭保持一致。當從內存中取單元到cache中時,會一次取一個cache line大小的內存區域到cache中,然後存進相應的cache line中。
如圖,兩顆CPU(核)讀取數據,如CPU1讀X,CPU2讀Y,Y與X處於同一個cache line,CPU1和CPU2會將X和Y所在的cache line全部讀進來,ALU與Register在讀取數據時,依次從L1、L2、L3、Main Memory中找數據,找到後按照相反的順序依次緩存數據,該過程會有損失,但是使用緩存的損失更低。如果兩顆CPU中間的數據需要保持一致性,如CPU1中X被修改,就必須告知CPU2中X已經過時,需要重新從內存中讀取,CPU內部的數據同步過程以cache line爲單位。這個協議稱爲緩存一致性協議,Intel採用的緩存一致性協議稱爲MESI Cache
一致性協議。MESI 是指4中狀態的首字母。每個Cache line有4個狀態,可用2個bit表示,它們分別是:
狀態 | 描述 | 監聽任務 |
---|---|---|
Modify(修改) | 該Cache line有效,數據被修改了,和內存中的數據不一致,數據只存在於本Cache中。 | 緩存行必須時刻監聽所有試圖讀該緩存行相對就主存的操作,這種操作必須在緩存將該緩存行寫回主存並將狀態變成S(共享)狀態之前被延遲執行。 |
Exclusive(獨享、互斥) | 該Cache line有效,數據和內存中的數據一致,數據只存在於本Cache中。 | 緩存行也必須監聽其它緩存讀主存中該緩存行的操作,一旦有這種操作,該緩存行需要變成S(共享)狀態。 |
Shared(共享) | 該Cache line有效,數據和內存中的數據一致,數據存在於很多Cache中。 | 緩存行也必須監聽其它緩存使該緩存行無效或者獨享該緩存行的請求,並將該緩存行變成無效(Invalid)。 |
Invalid(無效) | 該Cache line無效。 | 無 |
MESI Cache一致性協議是緩存鎖的實現之一,還有MSI、MOSI、Synapse、Firefly、Dragon。有些無法被緩存的數據,或者跨越多個cache line的數據,依然必須使用總線鎖,效率較低。下表示意了,當一個cache line調整狀態的時候,另外一個cache line 需要調整的狀態。
Modify | Exclusive | Shared | Invalid | |
---|---|---|---|---|
Modify | X | X | X | √ |
Exclusve | X | X | X | √ |
Shared | X | X | √ | √ |
Invalid | √ | √ | √ | √ |
cache line越大,局部性空間效率越高,但讀取時間慢;cache line越小,局部性空間效率越低,但讀取時間快;取一個折中值,目前多用64字節。下列程序展示修改的兩個數在不在同一個cache line情況下的時間消耗。
// 在同一cache line
public class SameCacheLine {
public static volatile long[] array = new long[2];
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 10_0000_0000L ; i++) {
array[0] = i;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10_0000_0000L ; i++) {
array[1] = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime()-start)/100_0000);
}
}
// 輸出:4366
----------------------------------------------------------------------------
// 不在同一cache line
public class DifferentCacheLine {
public static volatile long[] array = new long[16];
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 10_0000_0000L ; i++) {
array[0] = i;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10_0000_0000L ; i++) {
array[8] = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime()-start)/100_0000);
}
}
// 輸出:2276
cache line對齊:
對於有些特別敏感的數字,會存在線程高競爭的訪問,爲了保證不發生僞共享,可以使用緩存航對齊的編程方式。在著名的框架Disruptor中就應用了cache line對齊,如下所示:
public long p1, p2, p3, p4, p5, p6, p7; // cache line padding
private volatile long cursor = INITIAL_CURSOR_VALUE;
public long p8, p9, p10, p11, p12, p13, p14; // cache line padding
JDK7中,很多采用long padding提高效率;JDK8,加入了@Contended
註解,被註解的類屬性不與該類的其他屬性處於同一cache line,需要加上:JVM -XX:-RestrictContended
。因爲前後加上7個long類型進行padding只對Inter的CPU有效,通過使用@Contended
註解,虛擬機可以自動匹配CPU實現cache line對齊。
public class ContendedCacheLine {
@Contended
volatile long x;
@Contended
volatile long y;
public static void main(String[] args) throws InterruptedException {
ContendedCacheLine cacheLine = new ContendedCacheLine();
Thread t1 = new Thread(()->{
for (int i = 0; i < 10_0000_0000L ; i++) {
cacheLine.x = i;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10_0000_0000L ; i++) {
cacheLine.y = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime()-start)/100_0000);
}
}
// 輸出:
加上@Contended:5399
不加@Contended: 28908