IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

写Java也得了解CPU缓存

忘我的追寻 2014-04-13 22:39:18 累计浏览 5,544 次
本机暂存

   CPU,一般认为写C/C++的才需要了解,写高级语言的(Java/C#/pathon…)并不需要了解那么底层的东西。我一开始也是这么想的,但直到碰到LMAX的Disruptor,以及马丁的博文,才发现写Java的,更加不能忽视CPU。经过一段时间的阅读,希望总结一下自己的阅读后的感悟。本文主要谈谈CPU缓存对Java编程的影响,不涉及具体CPU缓存的机制和实现。

   现代CPU的缓存结构一般分三层,L1,L2和L3。如下图所示:

   CPU三级缓存示意图

   级别越小的缓存,越接近CPU, 意味着速度越快且容量越少。

   L1是最接近CPU的,它容量最小,速度最快,每个核上都有一个L1 Cache(准确地说每个核上有两个L1 Cache, 一个存数据 L1d Cache, 一个存指令 L1i Cache);

   L2 Cache 更大一些,例如256K,速度要慢一些,一般情况下每个核上都有一个独立的L2 Cache;

   L3 Cache是三级缓存中最大的一级,例如12MB,同时也是最慢的一级,在同一个CPU插槽之间的核共享一个L3 Cache。

   当CPU运作时,它首先去L1寻找它所需要的数据,然后去L2,然后去L3。如果三级缓存都没找到它需要的数据,则从内存里获取数据。寻找的路径越长,耗时越长。所以如果要非常频繁的获取某些数据,保证这些数据在L1缓存里。这样速度将非常快。下表表示了CPU到各缓存和内存之间的大概速度:

   从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

   利用CPU-Z可以查看CPU缓存的信息:

   CPU-Z

   在linux下可以使用下列命令查看proc文件系统或者sys下的设备描述。

   有了上面对CPU的大概了解,我们来看看缓存行(Cache line)。缓存,是由缓存行组成的。一般一行缓存行有64字节(由上图”64-byte line size”可知)。所以使用缓存时,并不是一个一个字节使用,而是一行缓存行、一行缓存行这样使用;换句话说,CPU存取缓存都是按照一行,为最小单位操作的。

   这意味着,如果没有好好利用缓存行的话,程序可能会遇到性能的问题。可看下面的程序:

public class L1CacheMiss {
    private static final int RUNS = 10;
    private static final int DIMENSION_1 = 1024 * 1024;
    private static final int DIMENSION_2 = 6;

    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....");

        long sum = 0L;
        for (int r = 0; r < RUNS; r++) {

            final long start = System.nanoTime();

            //slow
//            for (int j = 0; j < DIMENSION_2; j++) {
//                for (int i = 0; i < DIMENSION_1; i++) {
//                    sum += longs[i][j];
//                }
//            }

            //fast
            for (int i = 0; i < DIMENSION_1; i++) {
                for (int j = 0; j < DIMENSION_2; j++) {
                    sum += longs[i][j];
                }
            }

            System.out.println((System.nanoTime() - start));
        }

    }
}

   以我所使用的Xeon E3 CPU和64位操作系统和64位JVM为例,如这里所说,假设编译器采用行主序存储数组。

   64位系统,Java数组对象头固定占16字节(未证实),而long类型占8个字节。所以16+8*6=64字节,刚好等于一条缓存行的长度:

   缓存行效果

   如32-36行代码所示,每次开始内循环时,从内存抓取的数据块实际上覆盖了longs[i][0]到longs[i][5]的全部数据(刚好64字节)。因此,内循环时所有的数据都在L1缓存可以命中,遍历将非常快。

   假如,将32-36行代码注释而用25-29行代码代替,那么将会造成大量的缓存失效。因为每次从内存抓取的都是同行不同列的数据块(如longs[i][0]到longs[i][5]的全部数据),但循环下一个的目标,却是同列不同行(如longs[0][0]下一个是longs[1][0],造成了longs[0][1]-longs[0][5]无法重复利用)。运行时间的差距如下图,单位是微秒(us):

   程序运行效果对比

   最后,我们都希望需要的数据都在L1缓存里,但事实上经常事与愿违,所以缓存失效 (Cache Miss)是常有的事,也是我们需要避免的事。

   一般来说,缓存失效有三种情况:

   1. 第一次访问数据, 在cache中根本不存在这条数据, 所以cache miss, 可以通过prefetch解决。

   2. cache冲突, 需要通过补齐来解决(伪共享的产生)。

   3. cache满, 一般情况下我们需要减少操作的数据大小, 尽量按数据的物理顺序访问数据。

   参考:

   http://mechanitis.blogspot.hk/2011/07/dissecting-disruptor-why-its-so-fast_22.html

   http://coderplay.iteye.com/blog/1485760

   http://en.wikipedia.org/wiki/CPU_cache

   转载信息:原文地址

同分类推荐文章

  1. 等了十年的 Go 链式管道,终于来了:seq 让你像写 Scala 一样写 Go (2026-06-25 18:38:18)
  2. Go 实验特性详解 (2026-06-21 10:05:27)
  3. amd64 微架构级别对 Go 程序性能提升多少? (2026-06-21 09:38:49)

查看更多 后端 文章 →

建议继续学习

  1. SmartSprites - 命令行形式的CSS Sprites生成器 (累计阅读 123,894)
  2. Java开发岗位面试题归类汇总 (累计阅读 22,153)
  3. android 开发入门 (累计阅读 19,526)
  4. WEB系统需要关注的一些点 (累计阅读 18,215)
  5. 我的PHP,Python和Ruby之路 (累计阅读 13,145)
  6. HashMap解决hash冲突的方法 (累计阅读 12,652)
  7. 一种高效无锁内存队列的实现 (累计阅读 12,091)
  8. 一个大二学生有关如何成为一名软件工程师的疑问及答复 (累计阅读 9,176)
  9. 大并发下的高性能编程 – 改进的(用户态)自旋锁 (累计阅读 9,039)
  10. 低成本和高性能MySQL云数据的架构探索 (累计阅读 8,580)