CPU,通常認爲寫C/C++的才須要瞭解,寫高級語言的(Java/C#/pathon...)並不須要瞭解那麼底層的東西。我一開始也是這麼想的,但直到碰到LMAX的Disruptor,以及馬丁的博文,才發現寫Java的,更加不能忽視CPU。通過一段時間的閱讀,但願總結一下本身的閱讀後的感悟。本文主要談談CPU緩存對Java編程的影響,不涉及具體CPU緩存的機制和實現。java
現代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存取緩存都是按照一行,爲最小單位操做的。
這意味着,若是沒有好好利用緩存行的話,程序可能會遇到性能的問題。可看下面的程序:
public class L1CacheMiss { private static final int RUNS = 10; private static final int DIMENSION_1 = 1024 * 1024; private static final int DIMENSION_2 = 6; private static long[][] longs; public static void main(String[] args) throws Exception { longs = new long[DIMENSION_1][]; for (int i = 0; i < DIMENSION_1; i++) { longs[i] = new long[DIMENSION_2]; for (int j = 0; j < DIMENSION_2; j++) { longs[i][j] = 0L; } } System.out.println("starting...."); for (int r = 0; r < RUNS; r++) { long sum = 0L; final long start = System.nanoTime(); /** * slow * 由於每次從內存抓取的都是同行不一樣列的數據塊(如 longs[i][0]到longs[i][5]的所有數據),但循環下一個的目標, * 倒是同列不一樣行(如longs[0][0]下一個是longs[1] [0],形成了longs[0][1]-longs[0][5]沒法重複利用) * (SLOW)starting.... * 54166174 58234807 48707313 48018483 49324352 48286925 47918095 48829771 48263115 48003101 */ // for (int j = 0; j < DIMENSION_2; j++) { // for (int i = 0; i < DIMENSION_1; i++) { // sum += longs[i][j]; // } // } /** * fast * 每次開始內循環時,從內存抓取的數據塊實際上覆蓋了longs[i][0]到longs[i][5]的所有數據(恰好64字節)。 * 所以,內循環時全部的數據都在L1緩存能夠命中,遍歷將很是快。 * (FAST)starting.... * 19161832 20995203 13478486 13570992 13551230 13894658 14672971 13561262 13169205 13146782 */ for (int i = 0; i < DIMENSION_1; i++) { for (int j = 0; j < DIMENSION_2; j++) { sum += longs[i][j]; } } System.out.print((System.nanoTime() - start) + " "); } } }
以我所使用的Xeon E3 CPU和64位操做系統和64位JVM爲例,如這裏所說,假設編譯器採用行主序存儲數組。
64位系統,Java數組對象頭固定佔16字節(未證明),而long類型佔8個字節。因此16+8*6=64字節,恰好等於一條緩存行的長度:
如42-46行代碼所示,每次開始內循環時,從內存抓取的數據塊實際上覆蓋了longs[i][0]到longs[i][5]的所有數據(恰好64字節)。所以,內循環時全部的數據都在L1緩存能夠命中,遍歷將很是快。
假如,將32-36行代碼註釋而用29-34行代碼代替,那麼將會形成大量的緩存失效。由於每次從內存抓取的都是同行不一樣列的數據塊(如 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滿, 通常狀況下咱們須要減小操做的數據大小, 儘可能按數據的物理順序訪問數據。