CPU高速缓存Cache

概述 cpu的cache是一种又小又快的存储器,现在一般的cpu主流的cache是用sram,因为CPU的性能比Memory快得多,所以使用cache来拟补之间的差距

在计算机系统中,CPU高速缓存是用于减少处理器访问内存所需平均时间的部件。在金字塔式存储体系中它位于自顶向下的第二层,仅次于CPU寄存器。其容量远小于内存,但速度却可以接近处理器的频率。
当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。
缓存之所以有效,主要是因为程序运行时对内存的访问呈现局部性(Locality)特征。这种局部性既包括空间局部性(Spatial Locality),也包括时间局部性(Temporal Locality)。有效利用这种局部性,缓存可以达到极高的命中率。
在处理器看来,缓存是一个透明部件。因此,程序员通常无法直接干预对缓存的操作。但是,确实可以根据缓存的特点对程序代码实施特定优化,从而更好地利用缓存。
— 维基百科
CPU VS Memory
CPU高速缓存Cache
文章图片
CPU VS Memory 【CPU高速缓存Cache】在线主流的CPU中一般分为3级缓存,分别是L1,L2,L3,而L1缓存分为L1i(存储指令)和L1d(存储数据),L1缓存较小,其次就是L2缓存,它不分指令和数据,然后到L3,L3是最大的是所有核心公用的

CPU高速缓存Cache
文章图片
Cache Intel i7-4770 (Haswell), 3.4 GHz (Turbo Boost off)
性能介绍
CPU高速缓存Cache
文章图片
Intel i7-4770
One cycle: 1/CPU频率,i7-4770频率为3.4GHz,所以1cycle=1/3.4=0.29ns
那么Intel i7-4770的CPU Cache

CPU高速缓存Cache
文章图片
cache 32KB,64B/line,8-WAY的意思就是:
Cache总大小为32KB,8路组相连(每组有8个line),每个line的大小为64Byte,我们可以很轻易的算出一共有32K/8/64=64 个组。
从上图可以看出越靠近核心容量越小,但是速度越快
MESI(缓存一致性)
多核心CPU工作情况下就需要保证缓存的一致性:用于保证多个CPU cache之间缓存共享数据的一致。至于MESI,则是缓存一致性协议中的一个,到底怎么实现,还是得看具体的处理器指令集。
  1. 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
横向遍历的时候:
  1. 当尝试访问array[0][0]的时候,CPU Cache Miss。
  2. CPU尝试访问主存,操作系统一次访问的单位是一个 Cache Line 的大小64B,这意味着:一次性就会将array[0][0]~array[0][8]的数据读取到CPU Cache中(数组内存地址的连续性)
  3. 当第二次访问array[0][1]时,CPU访问Cache,发现有数据,Cache Hit返回array[0][1],而不用访问主存
    以此类推下面的所有步骤
纵向遍历的时候:
你就会发现每一次都是Cache Miss,因为1024 * 1024 * 8个Long(8B)=64M已经超出了L1,L2,L3的总缓存,即使是没有超过,CPU也不单单只有你这个程序在运行

    推荐阅读