概述
cpu的cache是一种又小又快的存储器,现在一般的cpu主流的cache是用sram,因为CPU的性能比Memory快得多,所以使用cache来拟补之间的差距
在计算机系统中,CPU高速缓存是用于减少处理器访问内存所需平均时间的部件。在金字塔式存储体系中它位于自顶向下的第二层,仅次于CPU寄存器。其容量远小于内存,但速度却可以接近处理器的频率。
当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。
缓存之所以有效,主要是因为程序运行时对内存的访问呈现局部性(Locality)特征。这种局部性既包括空间局部性(Spatial Locality),也包括时间局部性(Temporal Locality)。有效利用这种局部性,缓存可以达到极高的命中率。
在处理器看来,缓存是一个透明部件。因此,程序员通常无法直接干预对缓存的操作。但是,确实可以根据缓存的特点对程序代码实施特定优化,从而更好地利用缓存。
— 维基百科
CPU VS Memory
在线主流的CPU中一般分为3级缓存,分别是L1,L2,L3,而L1缓存分为L1i(存储指令)和L1d(存储数据),L1缓存较小,其次就是L2缓存,它不分指令和数据,然后到L3,L3是最大的是所有核心公用的
Intel i7-4770 (Haswell), 3.4 GHz (Turbo Boost off)
One cycle: 1/CPU频率,i7-4770频率为3.4GHz,所以1cycle=1/3.4=0.29ns
那么Intel i7-4770的CPU Cache
32KB,64B/line,8-WAY的意思就是:
Cache总大小为32KB,8路组相连(每组有8个line),每个line的大小为64Byte,我们可以很轻易的算出一共有32K/8/64=64 个组。
从上图可以看出越靠近核心容量越小,但是速度越快
MESI(缓存一致性)
多核心CPU工作情况下就需要保证缓存的一致性:用于保证多个CPU cache之间缓存共享数据的一致。至于MESI,则是缓存一致性协议中的一个,到底怎么实现,还是得看具体的处理器指令集。
- cache的写方式(详细可以看这一篇博客)
write through(写通):每次CPU修改了cache中的内容,立即更新到内存,也就意味着每次CPU写共享数据,都会导致总线事务,因此这种方式常常会引起总线事务的竞争,高一致性,但是效率非常低;
write back(写回):每次CPU修改了cache中的数据,不会立即更新到内存,而是等到cache line在某一个必须或合适的时机才会更新到内存中;
无论是写通还是写回,在多线程环境下都需要处理缓存cache一致性问题。为了保证缓存一致性,处理器又提供了写失效(write invalidate)和写更新(write update)两个操作来保证cache一致性。
- 写失效:当一个CPU修改了数据,如果其他CPU有该数据,则通知其为无效;
- 写更新:当一个CPU修改了数据,如果其他CPU有该数据,则通知其跟新数据;
MESI:modify、exclusive、shared、invalid,分别是修改、独占、共享和失效。
Cache MIss
如果发生了Cache Miss,就需要从Memory中取数据,这个取数据的过程中,CPU可以执行几十上百条指令的,如果等待数据时什么也不做时间就浪费了。可以在这个时候提高CPU使用效率的有两种方法,一个是乱序执行(out of order execution),即把当前线程中后面的、不依赖于当前指令执行结果的指令拿过来提前执行,另一个是超线程技术,即把另一个线程的指令拿过来执行
Cache Line(主角)
CacheLine 是CPU高速缓存中的最小单位,当从内存中取单元到cache中时,会一次取一个cacheline大小的内存区域到cache中,然后存进相应的cacheline中。
先看一个例子
public static void main(String[] args) {
long[][] array = new long[1024 * 1024][8];
long sum = 0;
//横向遍历
long start = System.currentTimeMillis();
for (int i = 0; i < 1024 * 1024; i += 1) {
for (int j = 0; j < 8; j++) {
sum += array[i][j];
}
}
System.out.println("row:" + (System.currentTimeMillis() - start) + "ms");
start = System.currentTimeMillis();
// 纵向遍历
for (int i = 0; i < 8; i += 1) {
for (int j = 0; j < 1024 * 1024; j++) {
sum += array[j][i];
}
}
System.out.println("column:" + (System.currentTimeMillis() - start) + "ms");
}
运行结果:
row:16ms
column:69ms
为什么会出现这样的情况
因为Cache line 的缘故,上面我们提及到了CPU的cache是多级的,但是下面我们可以忽略多级的概念,也忽略多核心访问Cache时,MESI(缓存一致性)带来的影响。
我的电脑是64位cache line 为 64B,意味着每一次读取64B到Cache中,在Java中 long是占用8B,所以8个Long就是8*8=64B
横向遍历的时候:
- 当尝试访问array[0][0]的时候,CPU Cache Miss。
- CPU尝试访问主存,操作系统一次访问的单位是一个 Cache Line 的大小64B,这意味着:一次性就会将array[0][0]~array[0][8]的数据读取到CPU Cache中(数组内存地址的连续性)
- 当第二次访问array[0][1]时,CPU访问Cache,发现有数据,Cache Hit返回array[0][1],而不用访问主存
以此类推下面的所有步骤
纵向遍历的时候:
你就会发现每一次都是Cache Miss,因为1024 * 1024 * 8个Long(8B)=64M已经超出了L1,L2,L3的总缓存,即使是没有超过,CPU也不单单只有你这个程序在运行