從Java視角理解系統結構(二)CPU緩存

從Java視角理解系統結構連載, 關注個人微博(連接)瞭解最新動態html

衆所周知, CPU是計算機的大腦, 它負責執行程序的指令; 內存負責存數據, 包括程序自身數據. 一樣你們都知道, 內存比CPU慢不少. 其實在30年前, CPU的頻率和內存總線的頻率在同一個級別, 訪問內存只比訪問CPU寄存器慢一點兒. 因爲內存的發展受到技術及成本的限制, 如今獲取內存中的一條數據大概須要200多個CPU週期(CPU cycles), 而CPU寄存器通常狀況下1個CPU週期就夠了.java

CPU緩存

網頁瀏覽器爲了加快速度,會在本機存緩存之前瀏覽過的數據; 傳統數據庫或NoSQL數據庫爲了加速查詢, 常在內存設置一個緩存, 減小對磁盤(慢)的IO. 一樣內存與CPU的速度相差太遠, 因而CPU設計者們就給CPU加上了緩存(CPU Cache). 若是你須要對同一批數據操做不少次, 那麼把數據放至離CPU更近的緩存, 會給程序帶來很大的速度提高. 例如, 作一個循環計數, 把計數變量放到緩存裏,就不用每次循環都往內存存取數據了. 下面是CPU Cache的簡單示意圖.shell

cpu cache

隨着多核的發展, CPU Cache分紅了三個級別: L1, L2, L3. 級別越小越接近CPU, 因此速度也更快, 同時也表明着容量越小. L1是最接近CPU的, 它容量最小, 例如32K, 速度最快,每一個核上都有一個L1 Cache(準確地說每一個核上有兩個L1 Cache, 一個存數據 L1d Cache, 一個存指令 L1i Cache). L2 Cache 更大一些,例如256K, 速度要慢一些, 通常狀況下每一個核上都有一個獨立的L2 Cache; L3 Cache是三級緩存中最大的一級,例如12MB,同時也是最慢的一級, 在同一個CPU插槽之間的核共享一個L3 Cache.數據庫

| 從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|編程

感興趣的同窗能夠在Linux下面用cat /proc/cpuinfo, 或Ubuntu下lscpu看看本身機器的緩存狀況, 更細的能夠經過如下命令看看:segmentfault

$ cat /sys/devices/system/cpu/cpu0/cache/index0/size
32K
$ cat /sys/devices/system/cpu/cpu0/cache/index0/type
Data
$ cat /sys/devices/system/cpu/cpu0/cache/index0/level
1
$ cat /sys/devices/system/cpu/cpu3/cache/index3/level
3

就像數據庫cache同樣, 獲取數據時首先會在最快的cache中找數據, 若是沒有命中(Cache miss) 則往下一級找, 直到三層Cache都找不到,那隻要向內存要數據了. 一次次地未命中,表明取數據消耗的時間越長.數組

緩存行(Cache line)

爲了高效地存取緩存, 不是簡單隨意地將單條數據寫入緩存的. 緩存是由緩存行組成的, 典型的一行是64字節.
讀者能夠經過下面的shell命令,查看cherency_line_size就知道知道機器的緩存行是多大.瀏覽器

$ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
 64

CPU存取緩存都是按行爲最小單位操做的. 在這兒我將不說起緩存的associativity問題, 將問題簡化一些. 一個Java long型佔8字節, 因此從一條緩存行上你能夠獲取到8個long型變量. 因此若是你訪問一個long型數組, 當有一個long被加載到cache中, 你將無消耗地加載了另外7個. 因此你能夠很是快地遍歷數組.緩存

實驗及分析

咱們在Java編程時, 若是不注意CPU Cache, 那麼將致使程序效率低下. 例如如下程序, 有一個二維long型數組, 在個人32位筆記本上運行時的內存分佈如圖:fetch

32位機器中的java的數組對象頭共佔16字節(詳情見 連接), 加上62個long型一行long數據一共佔512字節. 因此這個二維數據是順序排列的.

public class L1CacheMiss {
 private static final int RUNS = 10;
 private static final int DIMENSION_1 = 1024 * 1024;
 private static final int DIMENSION_2 = 62;

private static long[][] longs;

public static void main(String[] args) throws Exception {
 Thread.sleep(10000);
 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....");

final long start = System.nanoTime();
 long sum = 0L;
 for (int r = 0; r < RUNS; r++) {
// for (int j = 0; j < DIMENSION_2; j++) {
// for (int i = 0; i < DIMENSION_1; i++) {
// sum += longs[i][j];
// }
// }

for (int i = 0; i < DIMENSION_1; i++) {
 for (int j = 0; j < DIMENSION_2; j++) {
 sum += longs[i][j];
 }
 }
 }
 System.out.println("duration = " + (System.nanoTime() - start));
 }
}

編譯後運行,結果以下

$ java L1CacheMiss
starting....
duration = 1460583903

而後咱們將22-26行的註釋取消, 將28-32行註釋,
編譯後再次運行,結果是否是比咱們預想得還糟?

$ java L1CacheMiss
starting....
duration = 22332686898

前面只花了1.4秒的程序, 只作一行的對調要運行22秒. 從上節咱們能夠知道在加載longs[i][j]時, longs[i][j+1]極可能也會被加載至cache中, 因此當即訪問longs[i][j+1]將會命中L1 Cache, 而若是你訪問longs[i+1][j]狀況就不同了, 這時候極可能會產生 cache miss致使效率低下.

下面咱們用perf來驗證一下,先將快的程序跑一下.

$ perf stat -e L1-dcache-load-misses java L1CacheMiss
starting....
duration = 1463011588

Performance counter stats for 'java L1CacheMiss':

164,625,965 L1-dcache-load-misses

13.273572184 seconds time elapsed

一共164,625,965次L1 cache miss, 再看看慢的程序

$ perf stat -e L1-dcache-load-misses java L1CacheMiss
starting....
duration = 21095062165

Performance counter stats for 'java L1CacheMiss':

1,421,402,322 L1-dcache-load-misses

32.894789436 seconds time elapsed

這回產生了1,421,402,322次 L1-dcache-load-misses, 因此慢多了.

以上我只是示例了在L1 Cache滿了以後纔會發生的cache miss. 其實cache miss的緣由有下面三種:

  1. 第一次訪問數據, 在cache中根本不存在這條數據, 因此cache miss,能夠經過prefetch解決.
  2. cache衝突, 須要經過補齊來解決.
  3. 就是我示例的這種, cache滿, 通常狀況下咱們須要減小操做的數據大小, 儘可能按數據的物理順序訪問數據.

具體的信息能夠參考這篇論文.


by MinZhou via ifeve

相關文章
相關標籤/搜索