`
luozhonghua2014
  • 浏览: 62432 次
文章分类
社区版块
存档分类
最新评论

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代码收藏代码
  1. $cat/sys/devices/system/cpu/cpu0/cache/index0/size
  2. 32K
  3. $cat/sys/devices/system/cpu/cpu0/cache/index0/type
  4. Data
  5. $cat/sys/devices/system/cpu/cpu0/cache/index0/level
  6. 1
  7. $cat/sys/devices/system/cpu/cpu3/cache/index3/level
  8. 3

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

缓存行(Cache line)
为了高效地存取缓存, 不是简单随意地将单条数据写入缓存的. 缓存是由缓存行组成的, 典型的一行是64字节. 读者可以通过下面的shell命令,查看cherency_line_size就知道知道机器的缓存行是多大.
Shell代码收藏代码
  1. $cat/sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
  2. 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代码收藏代码
  1. publicclassL1CacheMiss{
  2. privatestaticfinalintRUNS=10;
  3. privatestaticfinalintDIMENSION_1=1024*1024;
  4. privatestaticfinalintDIMENSION_2=62;
  5. privatestaticlong[][]longs;
  6. publicstaticvoidmain(String[]args)throwsException{
  7. Thread.sleep(10000);
  8. longs=newlong[DIMENSION_1][];
  9. for(inti=0;i<DIMENSION_1;i++){
  10. longs[i]=newlong[DIMENSION_2];
  11. for(intj=0;j<DIMENSION_2;j++){
  12. longs[i][j]=0L;
  13. }
  14. }
  15. System.out.println("starting....");
  16. finallongstart=System.nanoTime();
  17. longsum=0L;
  18. for(intr=0;r<RUNS;r++){
  19. //for(intj=0;j<DIMENSION_2;j++){
  20. //for(inti=0;i<DIMENSION_1;i++){
  21. //sum+=longs[i][j];
  22. //}
  23. //}
  24. for(inti=0;i<DIMENSION_1;i++){
  25. for(intj=0;j<DIMENSION_2;j++){
  26. sum+=longs[i][j];
  27. }
  28. }
  29. }
  30. System.out.println("duration="+(System.nanoTime()-start));
  31. }
  32. }

编译后运行,结果如下
Shell代码收藏代码
  1. $javaL1CacheMiss
  2. starting....
  3. duration=1460583903

然后我们将22-26行的注释取消, 将28-32行注释, 编译后再次运行,结果是不是比我们预想得还糟?
Shell代码收藏代码
  1. $javaL1CacheMiss
  2. starting....
  3. 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代码收藏代码
  1. $perfstat-eL1-dcache-load-missesjavaL1CacheMiss
  2. starting....
  3. duration=1463011588
  4. Performancecounterstatsfor'javaL1CacheMiss':
  5. 164,625,965L1-dcache-load-misses
  6. 13.273572184secondstimeelapsed

一共164,625,965次L1 cache miss, 再看看慢的程序
Shell代码收藏代码
  1. $perfstat-eL1-dcache-load-missesjavaL1CacheMiss
  2. starting....
  3. duration=21095062165
  4. Performancecounterstatsfor'javaL1CacheMiss':
  5. 1,421,402,322L1-dcache-load-misses
  6. 32.894789436secondstimeelapsed

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

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

相关推荐

    java管理windows系统内存_java释放内存缓存_java获得CPU使用率_系统内存_硬盘_进程源代码

    "java管理windows系统内存_java释放内存缓存_java获得CPU使用率_系统内存_硬盘_进程源代码" 在Windows操作系统中,内存管理是一个非常重要的方面。Windows实现按需调页的虚拟内存机制,使得应用程序可以使用超过...

    java 缓存系统实战(安装和实例代码)

    - **Cache 定义**:Cache 通常被用来协调不同速度设备之间的数据传输,包括硬件级别的 CPU Cache 和软件层面的浏览器缓存、服务器缓存等。Memcached 属于广义上的 Cache,它是一个基于内存的分布式缓存系统。 - **...

    Cache的简单实现(java版)

    总结,通过Java实现缓存机制,不仅可以提升应用程序的性能,还能帮助开发者深入理解数据结构和算法,以及它们在实际问题中的应用。无论是随机、FIFO还是LRU,每种策略都有其适用场景,开发者应根据具体需求选择合适...

    java_cpu 内存模型和java内存模型.pdf

    理解MESI协议有助于Java程序员明白在多核处理器上,如何通过缓存一致性协议来管理不同CPU核心上的缓存数据,从而保证数据的正确性。 文件中还提到了其他一些CPU相关的概念,比如QPI代替FSB(快速通道互连替代前端...

    JAVA-缓存行对齐测试方法

    缓存行(Cache Line)是CPU缓存存储数据的基本单位,通常包含几十到几百个字节的数据。当多个线程同时访问不同但相邻的数据时,可能会引发竞争条件,导致性能下降,这种现象被称为缓存冲突或缓存不命中。在Java中,...

    CacheSim-master_java_cache仿真_cache仿真程序_cache_

    通过这个CacheSim-master项目,开发者和学习者可以更好地理解Cache的工作机制,掌握LRU算法的实现细节,以及如何在Java中编写高效的缓存管理代码。这对于优化系统性能、设计高效的数据访问策略等方面都有极大的实践...

    高速缓存实现源码

    为了评估和优化缓存性能,可以使用Java的性能分析工具,如JVisualVM、JProfiler等,进行CPU、内存和线程的监控。通过这些工具,我们可以发现潜在的性能瓶颈并进行优化。 在项目中,可能包含了以下核心部分: 1. ...

    JAVA缓存技术_深入了解.ppt

    **JAVA缓存技术详解** Java缓存技术是提高应用程序性能的关键技术之一,它通过存储频繁访问的数据,减少了对慢速资源(如磁盘或远程...理解缓存的工作原理和应用场景,并合理配置与监控,是每个Java开发者必备的技能。

    写Java也得了解CPU?CPU缓存

    本文主要谈谈CPU缓存对Java编程的影响,不涉及具体CPU缓存的机制和实现。  现代CPU的缓存结构一般分三层,L1,L2和L3。如下图所示:  级别越小的缓存,越接近CPU, 意味着速度越快且容量越少。  L1是接近...

    项目典型数据应用缓存的jvm之旅

    为了提高数据访问速度,JVM提供了一种称为“本地缓存”的机制,即硬件层面的CPU缓存,它能将频繁访问的数据暂时存储在高速缓存中,减少了主内存的访问次数,从而提升了性能。 二、Java缓存框架 在Java世界里,存在...

    基于硬件cache锁机制的Java虚拟机即时编译器优化

    硬件cache锁机制是利用CPU缓存来提高内存访问速度的一种技术,它通过缓存行的锁定来减少访问延迟和避免缓存行失效的情况。在多任务环境下,尤其是实时操作系统中,cache锁机制能够显著提高数据处理的实时性,因为...

    geoserver gwc缓存的生成与失效

    Geoserver GWC 缓存的生成与失效 Geoserver 是一个基于开源的 GIS 服务器,可以提供强大的地图服务功能。其中,GWC(GeoWebCache)是 Geoserver 中的一个重要组件,负责生成和管理地图缓存。缓存的生成和失效是 GWC...

    聊聊Java和CPU的关系

    volatile 可以确保共享变量在多线程环境下的可见性,它通过CPU的Lock指令实现,使得每次读取数据时都会从主内存中获取最新值,而不是使用CPU缓存。然而,volatile 无法保证原子性,例如对于`i++`这样的操作,需要...

    双缓存技术

    它主要通过利用两种不同级别的内存——通常是CPU缓存和硬盘上的磁盘缓存——来减少数据读取的延迟,提高用户体验。本文将深入探讨双缓存的概念、工作原理以及在实际Web应用中的应用。 首先,理解缓存的基本原理是至...

    MemCached Cache Java Client封装优化历程

    7. **简单的Cache机制**:无同步、消息分发等复杂机制,专注于核心的缓存功能,提供高效性能。 **Java客户端封装与优化** 在使用Java客户端与Memcached交互时,通常需要进行以下优化: 1. **连接池管理**:为了...

    先进先出缓存算法

    在给定的代码片段中,我们看到一个基于FIFO算法的CPU缓存模拟程序。这个程序旨在测试并评估FIFO缓存算法的性能,尤其是关注其在高并发请求下的表现。代码中包含了大量用于缓存管理的结构体定义和函数,这些是FIFO...

    缓存技术浅谈.zip

    它存在于多个层次,如CPU缓存、内存缓存、磁盘缓存甚至网络缓存。缓存的基本原理是利用局部性原理,即数据如果被访问一次,那么在不久的将来它很可能还会被再次访问。 在源码层面,开发者通常会使用各种编程语言...

    Java并发编程与高并发解决方案1

    假设我们有一个多核处理器系统,包括CPU A、B和C,以及相应的缓存cache a、cache b和cache c。在主内存中定义了一个变量x的引用值为0。 **单核读取流程:** 1. CPU A发出一条指令,从主内存中读取变量x。 2. 从主...

    Java内存模型知识汇总

    Java内存模型不仅涉及CPU缓存和主存的关系,还包括JVM堆内存与线程工作内存之间的关系。Java内存模型通过一套规则来定义了JVM堆内存和线程工作内存之间的交互。在Java中,所有的对象都存放在堆内存中,而每个线程有...

Global site tag (gtag.js) - Google Analytics