寫Java也得了解CPU--CPU緩存


CPU,一般認為寫C/C++的才需要了解,寫高級語言的(Java/C#/pathon...)並不需要了解那么底層的東西。我一開始也是這么想的,但直到碰到LMAX的Disruptor,以及馬丁的博文,才發現寫Java的,更加不能忽視CPU。經過一段時間的閱讀,希望總結一下自己的閱讀后的感悟。本文主要談談CPU緩存對Java編程的影響,不涉及具體CPU緩存的機制和實現。

 

現代CPU的緩存結構一般分三層,L1,L2和L3。如下圖所示:

 

級別越小的緩存,越接近CPU, 意味着速度越快且容量越少。

L1是最接近CPU的,它容量最小,速度最快,每個核上都有一個L1 Cache(准確地說每個核上有兩個L1 Cache, 一個存數據 L1d Cache, 一個存指令 L1i Cache);

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緩存的信息:

在linux下可以使用下列命令查看:

 

 

有了上面對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

http://coderplay.iteye.com/blog/1485760

http://en.wikipedia.org/wiki/CPU_cache


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM