写 Java 也得了解 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package  javaBasic;
 
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 {
         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...." );
 
         long  sum = 0L;
         for  ( int  r =  0 ; r < RUNS; r++) {
 
             final  long  start = System.nanoTime();
 
             // slow
             // for (int j = 0; j < DIMENSION_2; j++) {
             // for (int i = 0; i < DIMENSION_1; i++) {
             // sum += longs[i][j];
             // }
             // }
             /**
              * (FAST)starting.... 14612355 14250260 14112672 14470596 14266938
              * 14373097 14634805 14496253 14812805 14511328
             
             
              * (SLOW)starting.... 42201068 41150387 43047450 43338664 47759157
              * 42691770 42275796 41234736 41652314 41761680
              */
 
             // fast
             for  ( int  i =  0 ; i < DIMENSION_1; i++) {
                 for  ( int  j =  0 ; j < DIMENSION_2; j++) {
                     sum += longs[i][j];
                 }
             }
 
             System.out.println((System.nanoTime() - start));
         }
 
     }
}

 

以我所使用的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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值