Java也得了解CPU--CPU緩存

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滿, 通常狀況下咱們須要減小操做的數據大小, 儘可能按數據的物理順序訪問數據。

相關文章
相關標籤/搜索