从Java视角理解系统结构(二)CPU缓存
2016-12-21 20:08
316 查看
转载自并发编程网–ifeve.comhttp://ifeve.com/from-javaeye-cpu-cache/
众所周知,CPU是计算机的大脑,它负责执行程序的指令;内存负责存数据,包括程序自身数据.同样大家都知道,内存比CPU慢很多.其实在30年前,CPU的频率和内存总线的频率在同一个级别,访问内存只比访问CPU寄存器慢一点儿.由于内存
4000
的发展都到技术及成本的限制,
现在获取内存中的一条数据大概需要200多个CPU周期(CPUcycles),而CPU寄存器一般情况下1个CPU周期就够了.
CPU缓存
网页浏览器为了加快速度,会在本机存缓存以前浏览过的数据;传统数据库或NoSQL数据库为了加速查询,常在内存设置一个缓存,减少对磁盘(慢)的IO.同样内存与CPU的速度相差太远,于是CPU设计者们就给CPU加上了缓存(CPUCache).如果你需要对同一批数据操作很多次,那么把数据放至离CPU更近的缓存,会给程序带来很大的速度提升.例如,做一个循环计数,把计数变量放到缓存里,就不用每次循环都往内存存取数据了.下面是CPUCache的简单示意图.
随着多核的发展,CPUCache分成了三个级别:L1,L2,L3.级别越小越接近CPU,所以速度也更快,同时也代表着容量越小.L1是最接近CPU的,它容量最小,例如32K,速度最快,每个核上都有一个L1Cache(准确地说每个核上有两个L1Cache,一个存数据L1d
Cache,一个存指令L1iCache).L2Cache更大一些,例如256K,速度要慢一些,一般情况下每个核上都有一个独立的L2Cache;L3Cache是三级缓存中最大的一级,例如12MB,同时也是最慢的一级,在同一个CPU插槽之间的核共享一个L3Cache.
感兴趣的同学可以在Linux下面用cat/proc/cpuinfo,或Ubuntu下lscpu看看自己机器的缓存情况,更细的可以通过以下命令看看:
就像数据库cache一样,获取数据时首先会在最快的cache中找数据,如果没有命中(Cachemiss)则往下一级找,直到三层Cache都找不到,那只要向内存要数据了.一次次地未命中,代表取数据消耗的时间越长.
缓存行(Cacheline)
为了高效地存取缓存,不是简单随意地将单条数据写入缓存的.缓存是由缓存行组成的,典型的一行是64字节.读者可以通过下面的shell命令,查看cherency_line_size就知道知道机器的缓存行是多大.
CPU存取缓存都是按行为最小单位操作的.在这儿我将不提及缓存的associativity问题,将问题简化一些.一个Javalong型占8字节,所以从一条缓存行上你可以获取到8个long型变量.所以如果你访问一个long型数组,当有一个long被加载到cache中,你将无消耗地加载了另外7个.所以你可以非常快地遍历数组.
实验及分析
我们在Java编程时,如果不注意CPUCache,那么将导致程序效率低下.例如以下程序,有一个二维long型数组,在我的32位笔记本上运行时的内存分布如图:
32位机器中的java的数组对象头共占16字节(详情见链接),加上62个long型一行long数据一共占512字节.所以这个二维数据是顺序排列的.
编译后运行,结果如下
然后我们将22-26行的注释取消,将28-32行注释,编译后再次运行,结果是不是比我们预想得还糟?
前面只花了1.4秒的程序,只做一行的对调要运行22秒.从上节我们可以知道在加载longs[i][j]时,longs[i][j+1]很可能也会被加载至cache中,所以立即访问longs[i][j+1]将会命中L1Cache,而如果你访问longs[i+1][j]情况就不一样了,这时候很可能会产生cachemiss导致效率低下.
下面我们用perf来验证一下,先将快的程序跑一下.
一共164,625,965次L1cachemiss,再看看慢的程序
这回产生了1,421,402,322次L1-dcache-load-misses,所以慢多了.
以上我只是示例了在L1Cache满了之后才会发生的cachemiss.其实cachemiss的原因有下面三种:
1.第一次访问数据,在cache中根本不存在这条数据,所以cachemiss,可以通过prefetch解决.
2.cache冲突,需要通过补齐来解决.
3.就是我示例的这种,cache满,一般情况下我们需要减少操作的数据大小,尽量按数据的物理顺序访问数据.
众所周知,CPU是计算机的大脑,它负责执行程序的指令;内存负责存数据,包括程序自身数据.同样大家都知道,内存比CPU慢很多.其实在30年前,CPU的频率和内存总线的频率在同一个级别,访问内存只比访问CPU寄存器慢一点儿.由于内存
4000
的发展都到技术及成本的限制,
现在获取内存中的一条数据大概需要200多个CPU周期(CPUcycles),而CPU寄存器一般情况下1个CPU周期就够了.
CPU缓存
网页浏览器为了加快速度,会在本机存缓存以前浏览过的数据;传统数据库或NoSQL数据库为了加速查询,常在内存设置一个缓存,减少对磁盘(慢)的IO.同样内存与CPU的速度相差太远,于是CPU设计者们就给CPU加上了缓存(CPUCache).如果你需要对同一批数据操作很多次,那么把数据放至离CPU更近的缓存,会给程序带来很大的速度提升.例如,做一个循环计数,把计数变量放到缓存里,就不用每次循环都往内存存取数据了.下面是CPUCache的简单示意图.
随着多核的发展,CPUCache分成了三个级别:L1,L2,L3.级别越小越接近CPU,所以速度也更快,同时也代表着容量越小.L1是最接近CPU的,它容量最小,例如32K,速度最快,每个核上都有一个L1Cache(准确地说每个核上有两个L1Cache,一个存数据L1d
Cache,一个存指令L1iCache).L2Cache更大一些,例如256K,速度要慢一些,一般情况下每个核上都有一个独立的L2Cache;L3Cache是三级缓存中最大的一级,例如12MB,同时也是最慢的一级,在同一个CPU插槽之间的核共享一个L3Cache.
从CPU到 | 大约需要的CPU周期 | 大约需要的时间(单位ns) |
寄存器 | 1cycle | |
L1Cache | ~3-4cycles | ~0.5-1ns |
L2Cache | ~10-20cycles | ~3-7ns |
L3Cache | ~40-45cycles | ~15ns |
跨槽传输 | ~20ns | |
内存 | ~120-240cycles | ~60-120ns |
1 | $ cat/sys/devices/system/cpu/cpu0/cache/index0/size |
2 | 32K |
3 | $ cat/sys/devices/system/cpu/cpu0/cache/index0/type |
4 | Data |
5 | $ cat/sys/devices/system/cpu/cpu0/cache/index0/level |
6 | 1 |
7 | $ cat/sys/devices/system/cpu/cpu3/cache/index3/level |
8 | 3 |
缓存行(Cacheline)
为了高效地存取缓存,不是简单随意地将单条数据写入缓存的.缓存是由缓存行组成的,典型的一行是64字节.读者可以通过下面的shell命令,查看cherency_line_size就知道知道机器的缓存行是多大.
1 | $ cat/sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size |
2 | 64 |
实验及分析
我们在Java编程时,如果不注意CPUCache,那么将导致程序效率低下.例如以下程序,有一个二维long型数组,在我的32位笔记本上运行时的内存分布如图:
32位机器中的java的数组对象头共占16字节(详情见
01 | public class L1CacheMiss { |
02 | private static final int RUNS 10 ; |
03 | private static final int DIMENSION_1 1024 * 1024 ; |
04 | private static final int DIMENSION_2 62 ; |
05 |
06 | private static long [][] longs; |
07 |
08 | public static void main(String[] throws Exception { |
09 | Thread.sleep( 10000 ); |
10 | longs new long [DIMENSION_1][]; |
11 | for ( int i 0 ; |
12 | longs[i] new long [DIMENSION_2]; |
13 | for ( int j 0 ; |
14 | longs[i][j] |
15 | } |
16 | } |
17 | System.out.println( "starting...." ); |
18 |
19 | final long start |
20 | long sum |
21 | for ( int r 0 ; |
22 | // |
23 | // |
24 | // |
25 | // } |
26 | // } |
27 |
28 | for ( int i 0 ; |
29 | for ( int j 0 ; |
30 | sum |
31 | } |
32 | } |
33 | } |
34 | System.out.println( "duration + |
35 | } |
36 | } |
1 | $ |
2 | starting.... |
3 | duration |
1 | $ |
2 | starting.... |
3 | duration |
下面我们用perf来验证一下,先将快的程序跑一下.
1 | $ |
2 | starting.... |
3 | duration |
4 |
5 | Performance |
6 |
7 | 164,625,965 L1-dcache-load-misses |
8 |
9 | 13.273572184 |
1 | $ |
2 | starting.... |
3 | duration |
4 |
5 | Performance |
6 |
7 | 1,421,402,322 L1-dcache-load-misses |
8 |
9 | 32.894789436 |
以上我只是示例了在L1Cache满了之后才会发生的cachemiss.其实cachemiss的原因有下面三种:
1.第一次访问数据,在cache中根本不存在这条数据,所以cachemiss,可以通过prefetch解决.
2.cache冲突,需要通过补齐来解决.
3.就是我示例的这种,cache满,一般情况下我们需要减少操作的数据大小,尽量按数据的物理顺序访问数据.
相关文章推荐
- 从Java视角理解系统结构(二)CPU缓存
- 从Java视角理解系统结构(一)CPU上下文切换
- 从Java视角理解系统结构(三)伪共享
- 从Java视角理解系统结构(三)伪共享
- 从Java视角理解系统结构(一)CPU上下文切换
- 从Java视角理解系统结构(一)CPU上下文切换
- 从Java视角理解系统结构(一)CPU上下文切换
- 从Java视角理解系统结构(一)CPU上下文切换
- 从Java视角理解系统结构(三)伪共享
- 从Java视角理解系统结构(一)CPU上下文切换
- 从Java视角理解系统结构(一)CPU上下文切换
- 从Java视角理解CPU缓存(CPU Cache)
- 从Java视角理解CPU缓存(CPU Cache)
- 从Java视角理解CPU缓存和伪共享
- 从Java视角理解CPU缓存(CPU Cache)
- 从Java视角理解CPU缓存(CPU Cache)
- 从Java视角理解CPU缓存(CPU Cache)
- java 从Java视角理解CPU缓存(CPU Cache)
- 重JAVA视角理解CPU缓存
- 从Java视角理解CPU缓存(CPU Cache)