众所周知, CPU是计算机的大脑, 它负责执行程序的指令; 内存负责存数据, 包括程序自身数据. 同样大家都知道, 内存比CPU慢很多. 其实在30年前, CPU的频率和内存总线的频率在同一个级别, 访问内存只比访问CPU寄存器慢一点儿. 由于内存的发展都到技术及成本的限制, 现在获取内存中的一条数据大概需要200多个CPU周期(CPU cycles), 而CPU寄存器一般情况下1个CPU周期就够了.

CPU缓存
网页浏览器为了加快速度,会在本机存缓存以前浏览过的数据; 传统数据库或NoSQL数据库为了加速查询, 常在内存设置一个缓存, 减少对磁盘(慢)的IO. 同样内存与CPU的速度相差太远, 于是CPU设计者们就给CPU加上了缓存(CPU Cache). 如果你需要对同一批数据操作很多次, 那么把数据放至离CPU更近的缓存, 会给程序带来很大的速度提升. 例如, 做一个循环计数, 把计数变量放到缓存里,就不用每次循环都往内存存取数据了. 下面是CPU Cache的简单示意图.

随着多核的发展, CPU Cache分成了三个级别: L1, L2, L3. 级别越小越接近CPU, 所以速度也更快, 同时也代表着容量越小. L1是最接近CPU的, 它容量最小, 例如32K, 速度最快,每个核上都有一个L1 Cache(准确地说每个核上有两个L1 Cache, 一个存数据 L1d Cache, 一个存指令 L1i Cache). L2 Cache 更大一些,例如256K, 速度要慢一些, 一般情况下每个核上都有一个独立的L2 Cache; L3 Cache是三级缓存中最大的一级,例如12MB,同时也是最慢的一级, 在同一个CPU插槽之间的核共享一个L3 Cache.

从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

感兴趣的同学可以在Linux下面用cat /proc/cpuinfo, 或Ubuntu下lscpu看看自己机器的缓存情况, 更细的可以通过以下命令看看:

$ cat /sys/devices/system/cpu/cpu0/cache/index0/size
32K
$ cat /sys/devices/system/cpu/cpu0/cache/index0/type
Data
$ cat /sys/devices/system/cpu/cpu0/cache/index0/level $ cat /sys/devices/system/cpu/cpu3/cache/index3/level

就像数据库cache一样, 获取数据时首先会在最快的cache中找数据, 如果没有命中(Cache miss) 则往下一级找, 直到三层Cache都找不到,那只要向内存要数据了. 一次次地未命中,代表取数据消耗的时间越长.

缓存行(Cache line)
为了高效地存取缓存, 不是简单随意地将单条数据写入缓存的.  缓存是由缓存行组成的, 典型的一行是64字节. 读者可以通过下面的shell命令,查看cherency_line_size就知道知道机器的缓存行是多大.

$ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
 

CPU存取缓存都是按行为最小单位操作的. 在这儿我将不提及缓存的associativity问题, 将问题简化一些. 一个Java long型占8字节, 所以从一条缓存行上你可以获取到8个long型变量. 所以如果你访问一个long型数组, 当有一个long被加载到cache中, 你将无消耗地加载了另外7个. 所以你可以非常快地遍历数组.

实验及分析
我们在Java编程时, 如果不注意CPU Cache, 那么将导致程序效率低下. 例如以下程序, 有一个二维long型数组, 在我的32位笔记本上运行时的内存分布如图:


32位机器中的java的数组对象头共占16字节(详情见 链接), 加上62个long型一行long数据一共占512字节. 所以这个二维数据是顺序排列的.

 public class L1CacheMiss {
private static final int RUNS = 10;
private static final int DIMENSION_1 = 1024 * 1024;
private static final int DIMENSION_2 = 62; 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++) {
// for (int j = 0; j < DIMENSION_2; j++) {
// for (int i = 0; i < DIMENSION_1; i++) {
// sum += longs[i][j];
// }
// } for (int i = 0; i < DIMENSION_1; i++) {
for (int j = 0; j < DIMENSION_2; j++) {
sum += longs[i][j];
}
}
}
System.out.println("duration = " + (System.nanoTime() - start));
}
}

编译后运行,结果如下

$ java L1CacheMiss
starting....
duration = 1460583903

然后我们将22-26行的注释取消, 将28-32行注释, 编译后再次运行,结果是不是比我们预想得还糟?

$ java L1CacheMiss
starting....
duration = 22332686898

前面只花了1.4秒的程序, 只做一行的对调要运行22秒. 从上节我们可以知道在加载longs[i][j]时, longs[i][j+1]很可能也会被加载至cache中, 所以立即访问longs[i][j+1]将会命中L1 Cache, 而如果你访问longs[i+1][j]情况就不一样了, 这时候很可能会产生 cache miss导致效率低下.
下面我们用perf来验证一下,先将快的程序跑一下.

$ perf stat -e L1-dcache-load-misses java L1CacheMiss
starting....
duration = 1463011588 Performance counter stats for 'java L1CacheMiss': 164,625,965 L1-dcache-load-misses 13.273572184 seconds time elapsed

一共164,625,965次L1 cache miss, 再看看慢的程序

$ perf stat -e L1-dcache-load-misses java L1CacheMiss
starting....
duration = 21095062165 Performance counter stats for 'java L1CacheMiss': 1,421,402,322 L1-dcache-load-misses 32.894789436 seconds time elapsed

这回产生了1,421,402,322次 L1-dcache-load-misses, 所以慢多了.

以上我只是示例了在L1 Cache满了之后才会发生的cache miss. 其实cache miss的原因有下面三种:
1. 第一次访问数据, 在cache中根本不存在这条数据, 所以cache miss, 可以通过prefetch解决.
2. cache冲突, 需要通过补齐来解决.
3. 就是我示例的这种, cache满, 一般情况下我们需要减少操作的数据大小, 尽量按数据的物理顺序访问数据.
具体的信息可以参考这篇论文.

java cpu缓存的更多相关文章

  1. 从Java视角理解CPU缓存(CPU Cache)

    从Java视角理解系统结构连载, 关注我的微博(链接)了解最新动态众所周知, CPU是计算机的大脑, 它负责执行程序的指令; 内存负责存数据, 包括程序自身数据. 同样大家都知道, 内存比CPU慢很多 ...

  2. 基于JVM原理、JMM模型和CPU缓存模型深入理解Java并发编程

    许多以Java多线程开发为主题的技术书籍,都会把对Java虚拟机和Java内存模型的讲解,作为讲授Java并发编程开发的主要内容,有的还深入到计算机系统的内存.CPU.缓存等予以说明.实际上,在实际的 ...

  3. 从Java视角理解CPU缓存和伪共享

    转载自:http://ifeve.com/from-javaeye-cpu-cache/               http://ifeve.com/from-javaeye-false-shari ...

  4. 写Java也得了解CPU–CPU缓存

    CPU,一般认为写C/C++的才需要了解,写高级语言的(Java/C#/pathon…)并不需要了解那么底层的东西.我一开始也是这么想的,但直到碰到LMAX的Disruptor,以及马丁的博文,才发现 ...

  5. 【Java并发编程】从CPU缓存模型到JMM来理解volatile关键字

    目录 并发编程三大特性 原子性 可见性 有序性 CPU缓存模型是什么 高速缓存为何出现? 缓存一致性问题 如何解决缓存不一致 JMM内存模型是什么 JMM的规定 Java对三大特性的保证 原子性 可见 ...

  6. 第三章 - CPU缓存结构和java内存模型

    CPU 缓存结构原理 CPU 缓存结构 查看 cpu 缓存 速度比较 查看 cpu 缓存行 cpu 拿到的内存地址格式是这样的 CPU 缓存读 根据低位,计算在缓存中的索引 判断是否有效 0 去内存读 ...

  7. 并发研究之CPU缓存一致性协议(MESI)

    CPU缓存一致性协议MESI CPU高速缓存(Cache Memory) CPU为何要有高速缓存 CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU.这就造 ...

  8. C和C++中的volatile、内存屏障和CPU缓存一致性协议MESI

    目录 1. 前言2 2. 结论2 3. volatile应用场景3 4. 内存屏障(Memory Barrier)4 5. setjmp和longjmp4 1) 结果1(非优化编译:g++ -g -o ...

  9. CPU缓存刷新的误解

    即使是资深的技术人员,我经常听到他们谈论某些操作是如何导致一个CPU缓存的刷新.看来这是关于CPU缓存如何工作和缓存子系统如何与执行核心交互的一个常见误区.本文将致力于解释CPU缓存的功能以及执行程序 ...

随机推荐

  1. SQL高级优化之经常使用的优化策略-2(The Return Of The King)

    1.2 索引 索引不是越多越好,你须要知道索引建立多了.写入数据的效率会减少.怎样使用索引要看你的项目的应用场景,做出合理的測试评估. 1.2.1 统计数量 统计数量上.假设字段(fieldName) ...

  2. Linux下查看系统配置

    CPU 1. lscpu:显示cpu架构信息 [xxx@localhost ~]$ lscpu Architecture: x86_64 CPU op-mode(s): -bit, -bit Byte ...

  3. java 流程执行 循环 foreach循环

    一. if分支 1. 结构  if  else if   else 2.执行原则 if  if  if 结构  会一直去执行()里的判断语句 if else if  else if 结构  只要一条( ...

  4. Linux下git的配置

    需求和环境 硬件:一台装有Ubuntu14.04的PC 软件:git-core.openssh-server.openssh-client Git的安装 sudo apt-get install gi ...

  5. 第四篇:web之前端之jquery

    前端之jquery   前端之jquery 本节内容 jquery简介 选择器和筛选器 操作元素 示例 1. jquery简介 1 jquery是什么 jQuery由美国人John Resig创建,至 ...

  6. 关于sql 资源竞争死锁现象

    问题:System.Exception: 事务(进程 ID 321)与另一个进程被死锁在 锁 | 通信缓冲区 资源上,并且已被选作死锁牺牲品.请重新运行该事务 死锁最深层的原因就是一个:资源竞争 表现 ...

  7. [访问系统] Api_Win32_Mac类工具包 (转载)

    点击下载 Api_Win32_Mac.zip using System; using System.Collections.Generic; using System.Linq; using Syst ...

  8. Java的jLinqer包介绍

    1.介绍 熟悉C#的lambda 表达式,想要使用 LINQ 在 Java,是个问题,虽然 Java8用函数式接口已经实现了部分lambda 表达式,但是还是不够灵活. 2.Linqer简介 Linq ...

  9. JSP include HTML出现乱码

    解决方法:在项目的web.xml中加入下面语句:<jsp-config>     <jsp-property-group>     <description>    ...

  10. 深入理解shared pool共享池之library cache的library cache pin系列三

    关于library cache相关的LATCH非常多,名称差不多,我相信一些人对这些概念还是有些晕,我之前也有些晕,希望此文可以对这些概念有个更为清晰的理解,本文主要学习library cache p ...