CPU,通常認爲寫C/C++的才須要瞭解,寫高級語言的(Java/C#/pathon...)並不須要瞭解那麼底層的東西。我一開始也是這麼想的,但直到碰到LMAX的Disruptor,以及馬丁的博文,才發現寫Java的,更加不能忽視CPU。通過一段時間的閱讀,但願總結一下本身的閱讀後的感悟。本文主要談談CPU緩存對Java編程的影響,不涉及具體CPU緩存的機制和實現。html
現代CPU的緩存結構通常分三層,L1,L2和L3。以下圖所示:linux
級別越小的緩存,越接近CPU, 意味着速度越快且容量越少。git
L1是最接近CPU的,它容量最小,速度最快,每一個核上都有一個L1 Cache(準確地說每一個核上有兩個L1 Cache, 一個存數據 L1d Cache, 一個存指令 L1i Cache);github
L2 Cache 更大一些,例如256K,速度要慢一些,通常狀況下每一個核上都有一個獨立的L2 Cache;編程
L3 Cache是三級緩存中最大的一級,例如12MB,同時也是最慢的一級,在同一個CPU插槽之間的核共享一個L3 Cache。數組
當CPU運做時,它首先去L1尋找它所須要的數據,而後去L2,而後去L3。若是三級緩存都沒找到它須要的數據,則從內存裏獲取數據。尋找的路徑越長,耗時越長。因此若是要很是頻繁的獲取某些數據,保證這些數據在L1緩存裏。這樣速度將很是快。下表表示了CPU到各緩存和內存之間的大概速度:緩存
從CPU到 大約須要的CPU週期 大約須要的時間(單位ns)
寄存器 1 cycle
L1 Cache ~3-4 cycles ~0.5-1 ns
L2 Cache ~10-20 cycles ~3-7 ns
L3 Cache ~40-45 cycles ~15 ns
跨槽傳輸 ~20 ns
內存 ~120-240 cycles ~60-120ns性能
利用CPU-Z能夠查看CPU緩存的信息:fetch
在linux下可使用下列命令查看:spa
有了上面對CPU的大概瞭解,咱們來看看緩存行(Cache line)。緩存,是由緩存行組成的。通常一行緩存行有64字節(由上圖"64-byte line size"可知)。因此使用緩存時,並非一個一個字節使用,而是一行緩存行、一行緩存行這樣使用;換句話說,CPU存取緩存都是按照一行,爲最小單位操做的。
這意味着,若是沒有好好利用緩存行的話,程序可能會遇到性能的問題。可看下面的程序:
1 public class L1CacheMiss { 2 private static final int RUNS = 10; 3 private static final int DIMENSION_1 = 1024 * 1024; 4 private static final int DIMENSION_2 = 6; 5 6 private static long[][] longs; 7 8 public static void main(String[] args) throws Exception { 9 Thread.sleep(10000); 10 longs = new long[DIMENSION_1][]; 11 for (int i = 0; i < DIMENSION_1; i++) { 12 longs[i] = new long[DIMENSION_2]; 13 for (int j = 0; j < DIMENSION_2; j++) { 14 longs[i][j] = 0L; 15 } 16 } 17 System.out.println("starting...."); 18 19 long sum = 0L; 20 for (int r = 0; r < RUNS; r++) { 21 22 final long start = System.nanoTime(); 23 24 //slow 25 // for (int j = 0; j < DIMENSION_2; j++) { 26 // for (int i = 0; i < DIMENSION_1; i++) { 27 // sum += longs[i][j]; 28 // } 29 // } 30 31 //fast 32 for (int i = 0; i < DIMENSION_1; i++) { 33 for (int j = 0; j < DIMENSION_2; j++) { 34 sum += longs[i][j]; 35 } 36 } 37 38 System.out.println((System.nanoTime() - start)); 39 } 40 41 } 42 }
以我所使用的Xeon E3 CPU和64位操做系統和64位JVM爲例,如這裏所說,假設編譯器採用行主序存儲數組。
64位系統,Java數組對象頭固定佔16字節(未證明),而long類型佔8個字節。因此16+8*6=64字節,恰好等於一條緩存行的長度:
如32-36行代碼所示,每次開始內循環時,從內存抓取的數據塊實際上覆蓋了longs[i][0]到longs[i][5]的所有數據(恰好64字節)。所以,內循環時全部的數據都在L1緩存能夠命中,遍歷將很是快。
假如,將32-36行代碼註釋而用25-29行代碼代替,那麼將會形成大量的緩存失效。由於每次從內存抓取的都是同行不一樣列的數據塊(如longs[i][0]到longs[i][5]的所有數據),但循環下一個的目標,倒是同列不一樣行(如longs[0][0]下一個是longs[1][0],形成了longs[0][1]-longs[0][5]沒法重複利用)。運行時間的差距以下圖,單位是微秒(us):
最後,咱們都但願須要的數據都在L1緩存裏,但事實上常常事與願違,因此緩存失效 (Cache Miss)是常有的事,也是咱們須要避免的事。
通常來講,緩存失效有三種狀況:
1. 第一次訪問數據, 在cache中根本不存在這條數據, 因此cache miss, 能夠經過prefetch解決。
2. cache衝突, 須要經過補齊來解決(僞共享的產生)。
3. cache滿, 通常狀況下咱們須要減小操做的數據大小, 儘可能按數據的物理順序訪問數據。
參考:
http://mechanitis.blogspot.hk/2011/07/dissecting-disruptor-why-its-so-fast_22.html