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

从Java视角理解系统结构连载, 关注我的微博(链接)了解最新动态众所周知, 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看看自己机器的缓存情况, 更细的可以通过以下命令看看:

Shell代码

  • $ 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
  • 1
  • $ cat /sys/devices/system/cpu/cpu3/cache/index3/level
  • 3

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

缓存行(Cache line)

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

Shell代码

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

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

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

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

Java代码

  • 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));
  • }
  • }

编译后运行,结果如下

Shell代码

  • $ java L1CacheMiss
  • starting….
  • duration = 1460583903

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

Shell代码

  • $ 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来验证一下,先将快的程序跑一下.

Shell代码

  • $ 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, 再看看慢的程序

Shell代码

  • $ 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满, 一般情况下我们需要减少操作的数据大小, 尽量按数据的物理顺序访问数据.

具体的信息可以参考这篇论文.

下一篇将介绍CPU cache的另一种误区: 伪共享(False Sharing).

时间: 2024-10-26 16:36:34

从Java视角理解CPU缓存(CPU Cache)的相关文章

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

CPU是计算机的大脑,它负责执行程序的指令:内存负责存数据,包括程序自身数据.内存比CPU慢很多,现在获取内存中的一条数据大概需要200多个CPU周期(CPU cycles),而CPU寄存器一般情况下1个CPU周期就够了.       网页浏览器为了加快速度,会在本机存缓存以前浏览过的数据:传统数据库或NoSQL数据库为了加速查询,常在内存设置一个缓存,减少对磁盘(慢)的IO.同样内存与CPU的速度相差太远,于是CPU设计者们就给CPU加上了缓存(CPU Cache).如果需要对同一批数据操作很

计算机底层知识拾遗(六)理解页缓存page cache和地址空间address_space

在这篇计算机底层知识拾遗(五)理解块IO层 中讲了块缓存buffer cache块缓存,这篇说说页缓存page cache以及相关的地址空间address_space的要点. 在Linux 2.4内核中块缓存buffer cache和页缓存page cache是并存的,表现的现象是同一份文件的数据,可能即出现在buffer cache中,又出现在页缓存中,这样就造成了物理内存的浪费.Linux 2.6内核对两个cache进行了合并,统一使用页缓存在做缓存,只有极少数的情况下才使用到buffer

轻松学会Java高并发第二课-CPU多级缓存

CPU多级缓存 为什么需要CPU缓存 CPU的频率越来越快,主存越来越跟不上CPU的变化,这样伴随的问题就是处理器的时钟周期内,CPU需要常常等待主存,浪费资源.所以缓存的出现,是为了缓解CPU和内存之间的速度不匹配问题. 一级缓存 CPU核心与主存之间有一层高速缓存.如下图(1)所示,数据的读取与存储都经过高速缓存.主存和缓存都连接在系统总线上(Bus). 多级缓存 CPU核心与主存之间有多级缓存.如下图(2)所示 缓存如何解决CPU和内存之间的速度问题(意义) 时间局部性:如果某个数据被访问

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

许多以Java多线程开发为主题的技术书籍,都会把对Java虚拟机和Java内存模型的讲解,作为讲授Java并发编程开发的主要内容,有的还深入到计算机系统的内存.CPU.缓存等予以说明.实际上,在实际的Java开发工作中,仅仅了解并发编程的创建.启动.管理和通信等基本知识还是不够的.一方面,如果要开发出高效.安全的并发程序,就必须深入Java内存模型和Java虚拟机的工作原理,从底层了解并发编程的实质:更进一步地,在现今大数据的时代,要开发出高并发.高可用.考可靠的分布式应用及各种中间件,更需要深

java cpu缓存

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

写Java也得了解CPU缓存

CPU,一般认为写C/C++的才需要了解,写高级语言的(Java/C#/pathon...)并不需要了解那么底层的东西.我一开始也是这么想的,但直到碰到LMAX的Disruptor,以及马丁的博文,才发现写Java的,更加不能忽视CPU.经过一段时间的阅读,希望总结一下自己的阅读后的感悟.本文主要谈谈CPU缓存对Java编程的影响,不涉及具体CPU缓存的机制和实现. 现代CPU的缓存结构一般分三层,L1,L2和L3.如下图所示: 级别越小的缓存,越接近CPU, 意味着速度越快且容量越少. L1是

一篇文章让你明白CPU缓存一致性协议MESI

CPU高速缓存(Cache Memory) CPU为何要有高速缓存 CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU.这就造成了高性能能的内存和硬盘价格及其昂贵.然而CPU的高度运算需要高速的数据.广州SEO为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存以解决I\O速度和CPU运算速度之间的不匹配问题. 在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理. **时间局部性(Tempora

CPU缓存机制 并发编程的基础

CPU多核缓存架构 1.多线程环境下存在的问题 在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(RAM).基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性(CacheCoherence). 若有两个线程 T1 和 T2 都去计算 x + 1的值(x初始值为1),T1线程的由 CPU1去处理, T2 线程由CPU2去处理:CPU1和CPU2的高速缓存中的副本都是 x = 1,经过CPU加1操作后,再次放入的CPU1.CPU2的高速缓

Linux下查询CPU 缓存的工具

在Linux下能够使用例如以下工具查询CPU缓存: 方式1: $ lscpu L1d cache: 32K <span style="white-space:pre"> </span>(一级数据缓存) L1i cache: 32K <span style="white-space:pre"> </span><span style="font-family: Arial, Helvetica, sans-