CPU缓存
网页浏览器为了加快速度,会在本机存缓存以前浏览过的数据; 传统数据库或NoSQL数据库为了加速查询, 常在内存设置一个缓存, 减少对磁盘(慢)的IO. 同样内存与CPU的速度相差太远, 于是CPU设计者们就给CPU加上了缓存(CPU Cache). 如果你需要对同一批数据操作很多次, 那么把数据放至离CPU更近的缓存, 会给程序带来很大的速度提升. 例如, 做一个循环计数, 把计数变量放到缓存里,就不用每次循环都往内存存取数据了.
现代CPU的缓存结构一般分三层,L1,L2和L3。如下图所示:
CPU Cache分成了三个级别: L1, L2, L3. 级别越小越接近CPU, 所以速度也更快, 同时也代表着容量越小.
1. L1是最接近CPU的, 它容量最小, 例如32K, 速度最快,每个核上都有一个L1 Cache(准确地说每个核上有两个L1 Cache, 一个存数据 L1d Cache, 一个存指令 L1i Cache).
2. L2 Cache 更大一些,例如256K, 速度要慢一些, 一般情况下每个核上都有一个独立的L2 Cache;
3. L3 Cache是三级缓存中最大的一级,例如12MB,同时也是最慢的一级, 在同一个CPU插槽之间的核共享一个L3 Cache.
缓存行
缓存行是缓存中可以分配的最小单位。在进行数据缓存的时候,并不是单纯的将单条数据读入缓存,而是以缓存行的大小进行读写,一般典型的大小是64字节。
CPU存取缓存都是按行为最小单位操作的. 在这儿我将不提及缓存的associativity问题, 将问题简化一些. 一个Java long型占8字节, 所以从一条缓存行上你可以获取到8个long型变量. 所以如果你访问一个long型数组, 当有一个long被加载到cache中, 你将无消耗地加载了另外7个. 所以你可以非常快地遍历数组.
测试缓存行对程序性能的影响
64位系统,Java数组对象头固定占16字节(IBM-java对象字节分析),而long类型占8个字节。所以16+8*6=64字节,刚好等于一条缓存行的长度:
linux下查看缓存行大小(未验证):
$ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
测试代码:
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....");
final long start = System.nanoTime();
long sum = 0L;
for (int r = 0; r < RUNS; r++) {
//slow
// for (int j = 0; j < DIMENSION_2; j++) {
// for (int i = 0; i < DIMENSION_1; i++) {
// sum += longs[i][j];
// }
// }
//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));
}
}
运行结果分析:
1. 代码30-34行,遍历顺序是longs[i][0]–longs[i][5],刚好6*8+16=64字节。等于一个缓存行的大小。所以速度很快
2. 代码22-27行,遍历顺序是longs[0][i]–longs[5][i],每次都导致缓存未命中。速度很慢。
一般来说,缓存失效有三种情况:
第一次访问数据, 在cache中根本不存在这条数据, 所以cache miss, 可以通过prefetch解决。
cache冲突, 需要通过补齐来解决(伪共享的产生)。
cache满, 一般情况下我们需要减少操作的数据大小, 尽量按数据的物理顺序访问数据。