`
in355hz
  • 浏览: 227871 次
社区版块
存档分类
最新评论

从一个 JDK6 BUG 看 JAVA 数组创建

阅读更多

前几天在调试一段程序, 奇怪的发现: 程序性能和缓存大小是负相关的——缓存开越大,程序越慢,最快和最慢之间差 2 个数量级。开 jprofiler 查了下,发现了 JDK6 里有这样一段代码 (版本 build 1.6.0_14-b08):

红线里的部分只能用笔误来形容 ——Arrays.copyOf() 复制了我传递的整个缓存——在我的测试里,当缓存大小是 8KB 时,复制大约需要 3000ns,而缓存大小是 64KB 时,复制需要 21000ns.


显然,使得 Arrays.copyOf 很慢的原因是每次创建新数组,造成 CPU cache 利用率不高,下面来证明一下。

Arrays.copyOf 的执行流程需要以下三步:

1. JVM 在堆中分配 8KB 内存;
    Java 堆分配是 O(1), 顺序标记下一块 8KB 已经被分配了。
    这基本可以看作不需要时间,实测约 150ns.

2. JVM 初始化 8KB 内存为 { 0 }
    Java 会自动给新分配的数组元素置 0,即用 0 覆写新分配的内存。
    CPU 怎么执行覆写?简单来说,有两种策略: Write-Through 和 Write-Back(注1).
    策略 Write-Through 是无视 cache 直接覆写主内存 RAM,而 Write-Back 首会先从 RAM 读数据到 cache, 然后再覆写 cache, 标记 cacheline 为 dirty. 
    不管走什么策略,因为新分配的内存基本还在主存 RAM 里,覆写过程都需要访问 RAM,传输 8KB 的数据。
    假设 JVM 优化得不错,能足够利用总线带宽。则可以这么估计:我机器的内存是 DDR3 1066@533MHz, 每次可以传输 8 x 64 / 8 = 64B, 延时 13.125ns (DDR3 1066),  一共需要 1680ns. 
   如果是 Write-Through,则总延时 = 写 RAM 延时 = 1680ns.
   如果是 Write-Back,除了从 RAM 读出的时间外,还需要估算中考虑 L1 的写入时间,假设 JVM 代码足够优化,使用 64B cacheline 粒度写入:
   估计值 = 1680ns + 8KB / 64B * 2 cycle / 2.4GHz (注2)= 1787ns

    实际验证,在我的机器上需要 1900-2000ns.


   注 1:从网上查到了 Intel i5 CPU 的数据, 子型号不太匹配, 凑合一下: 
      L1 Read/Write: 2 cycles per read (throughput)
      L2->L1 B/W (Read, 64B) = 4 cycles per cache line
      L2 Write (Write, 64B) = 7 cycles per write (cache line)
      L3->L1 B/W (Read, 64B) = 6 cycles per cache line
      L3 Write (Write, 64B) = 11 cycles per write (cache line)
    下文也是用这个参数估算。

    注 2:更复杂的情况是,因为数组刚创建,访问首先会出现 cache miss,这时 CPU 会用 Write-Back(miss allocate) — 简称 Wite-Allocate 或者 Write-Back(through on miss) 策略,决定是先从下一级 cache 读出 cacheline 还是直接覆写下一级 cache.
        另外的复杂性是 CPU L1/L2/L3 cache 可能走不同的策略。

   如果 CPU 是 Write-Allocate 策略,初始化 { 0 } 完毕后,数组会全部加载到 cache. 这是 JAVA 创建数组的优势。

3. 拷贝 8KB 到新分配的内存
    System.arraycopy 是 native 调用,实现未知 (手头只能看到 OpenJDK 的代码), 优化不错的话, 应该是调用 rep movsd 或类似的指令。
    这里的复杂性在于,从源数组读可能会出现 cache-miss, 写入新数组也可能出现 cache-miss. 通常 CPU L1/L2 cache 的命中率保守是 80%,我们可以估算一下:

    如果 CPU 用 Write-Throught 策略, 这样读走 cache, 写直接写穿到 RAM, 很容易看出这样性能较低。
      读 L1  = 8KB / 64B * 2-cycles / 2.4GHz = 107ns
      读 L2  = 8KB / 64B * 4-cycles / 2.4GHz = 213ns
      读 L3  = 8KB / 64B * 6-cycles / 2.4GHz = 320ns
      写 RAM = 8KB / 64B * 13.125ns = 1680ns
      估计值 = L1 107ns + L2 213ns * 0.2 + L3 320ns * 0.04 + RAM 1680ns = 1842ns

    如果 CPU 用 Write-Allocate 或者 Write-Back (through on miss) 策略, 这样读写都走 cache,  并且 miss allocate 会引发 dirty cacheline 的回写: 
      读 L1  = 8KB / 64B * 2-cycles / 2.4GHz = 107ns
      读 L2  = 8KB / 64B * 4-cycles / 2.4GHz = 213ns
      读 L3  = 8KB / 64B * 6-cycles / 2.4GHz = 320ns
      写 L1  = 8KB / 64B * 2-cycles / 2.4GHz = 107ns
      写 L2  = 8KB / 64B * 7-cycles / 2.4GHz = 373ns
      写 L3  = 8KB / 64B * 11-cycles / 2.4GHz = 587ns
      W-A 估计值  = L1 107ns + L2 213ns * 0.2 + L3 320ns * 0.04 + 
                      L1 107ns + L2 213ns * 0.2 + L3 320ns * 0.04 + 
                      L2 373ns * 0.4 + L3 587ns * 0.08 = 521ns
      W-B 估计值  = L1 107ns + L2 213ns * 0.2 + L3 320ns * 0.04 + 
                      L1 107ns * 0.8 + L2 373ns * 0.16 + L3 587ns * 0.04 + 
                      L2 373ns * 0.2 + L3 587ns * 0.04 = 449ns

    实际验证一下,下面测出是 450-500ns:

    实测的结果证明 Intel i5 CPU 是用 Write-Back 策略管理缓存,实际上也是这样的。网上资料可以证明 Intel 自 486 CPU 后 cache 都采用 Write-Allocate 模式。

结果说明了什么?
   1) 因为需要在创建数组后置 0,  产生 cache miss,  导致 JAVA 创建数组所需时间和数组大小成正比。
   2) 作为上一条的副作用,JAVA 创建的数组已经进入 L2 cache (甚至 L1), 后续的访问效率很高。

我们应该怎样做?
   1) 在要求性能的代码中,尽量少创建数组,尽量复用上次创建的数组。
   2) 使用 HashMap 或者 ArrayList 之类内部依赖数组的数据结构时,初始大小不要太大,需要时再扩充比较好 (如果分配 256,实际需要 512,损失 50% 效率。如果分配 1024,实际需要 512,损失 100% 效率)。
   3) 创建数组和访问数组的代码尽量在一起。

其他: 有没考虑的因素吗?
    问题 1:覆写新数组没有考虑 L3 回写到 RAM 的影响?
    假设 L3 cache 策略是 Write-Allocate,如果产生 L3 cache miss, 除了要从 RAM 加载 cacheline, 还会造成被淘汰的 cacheline 回写。极限情况下, 每次 L3 cache miss 可能全部需要回写, 需要传输双倍的数据。
    如果有 30% 的 L3 cacheline 需要回写: 
     总计   = 1680ns + 1680ns * 0.3 = 2184ns

    问题 2:是否有 TLB miss 的影响? 
    Intel CPU 的内存页面大小是 4KB, 测试的工作集仅 16KB, 需要占用 4 个 TLB item. 
    如果全部 TLB miss, 仅仅会增加 4 * 7 cycles / 2.4GHz = 12ns.

    问题 3:GC 对性能的影响? 为了计算 GC 对计算的影响, 我打开了 -verbose:gc 选项:
     [GC 16582K->395K(260160K), 0.0002036 secs]
     [GC 16580K->395K(260160K), 0.0002074 secs]
     [GC 16585K->395K(260160K), 0.0002019 secs]
     ...
    在 -Xms256m -Xmx256m 参数下,代码每运行 20000 次产生 10 次 minor-gc, 每次大约 0.2ms, 这样影响是:
     实测   = 10 * 0.2ms / 20000 = 100ns
    有些影响, 但不至于影响结论。

    问题 4:换台机器,计算方法还有效吗?
    在服务器 (Xeon L5630 2.13GHz, 虚拟机) 上检验一下 (OpenJDK 64-Bit Server VM):
      (CPU 参数来自评测,Xeon 是服务器系列,主频稍低、延迟稍高)
      L1  = 8KB / 64B * 4-cycles / 2.13GHz = 240ns
      L2  = 8KB / 64B * 11-cycles / 2.13GHz = 661ns
      L3  = 8KB / 64B * 13-cycles / 2.13GHz = 781ns
      估计值 = L1 240ns + L2 661ns * 0.2 + L3 781ns * 0.04 + // 读
                     L1 240ns + L2 661ns * 0.2 +                              // 写
                     L2 661ns * 0.4 + L3 781ns * 0.04 = 1071ns
      实测值 = 1298ns
    虚拟机 dmidecode 命令不可用, 不知道内存类型, 但是服务器内存带 ECC 效验, 每 64bit 增加 8bit 效验位,多消耗 1 个 CPU cycle:
      估计值 = (8KB / 64 * 72) / 64B * (13.125ns + 1/2.13GHz) = 1958ns
      实测值 = 2458ns
分享到:
评论

相关推荐

    java-jdk-8u231-windowsx64修复bug.rar

    本版本修复 JDK的Bug1–数组切割 JDK的Bug2–三元运算符 JDK中不算Bug的Bug–ArrayList可通过构造函数传入非指定泛型的List并在get时出错

    java的jdk6安装包

    java的jdk6安装包

    java jdk1.8java jdk1.8

    java jdk1.8java jdk1.8java jdk1.8java jdk1.8java jdk1.8java jdk1.8java jdk1.8java jdk1.8java jdk1.8java jdk1.8java jdk1.8java jdk1.8java jdk1.8java jdk1.8java jdk1.8java jdk1.8java jdk1.8java jdk1.8...

    JDK中文手册(JAVA).rar

    JDK中文手册(JAVA),JDK API手册

    Java JDK6开发工具

    JDK是整个Java的核心,包括了Java运行环境(Java Runtime Envirnment),一堆Java工具和Java基础的类库(rt.jar).不论什么Java应用服务器实质都是内置了某个版本的JDK. 因此掌握 JDK是学好Java的第一步.最主流的JDK是Sun...

    Professional Java JDK 6 Edition

    学习java JDK 6 必不可少的资源

    jdk_api_1.8-JAVA中文版API手册

    1)Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可。 2)新增lambda表达式 3)提供函数式接口 4)Java 8 允许你使用关键字来传递方法或者构造函数引用 5)我们可以直接在lambda表达式...

    java jdk6

    java jdk6 java jdk6 java jdk6 java jdk6 java jdk6 java jdk6 java jdk6

    mac jdk6 java6支持最新macOs10.13.4版本及以下

    mac jdk6 java6支持最新macOs10.13.4版本及以下,亲测有效

    《Java JDK6学习笔记》

    Java、JDK6、良葛、林信良、Java学习笔记 我没看到实体书,不知是否相同, 近些日学习一下ruby方面的,顺便看看java方面的, 这个是Java JDK6的学习笔记电子版, 压缩包里包含两个文件, 一个是chm格式(这个还行吧)...

    Java JDK API 1.6 官方版下载

    jdk1.6方便别人使用不用从官网上查找,主要用于java开发.JDK Java Development Kit是SunMicrosystems针对Java开发员的产品。自从Java推出以来,JDK已经成为使用最广泛的JavaSDK,本站提供java jdk1.6官方版下载。JDK...

    java jdk 8 帮助文档 中文 文档 chm 谷歌翻译

    JDK1.8 API 中文谷歌翻译版 java帮助文档 JDK API java 帮助文档 谷歌翻译 JDK1.8 API 中文 谷歌翻译版 java帮助文档 Java最新帮助文档 本帮助文档是使用谷歌翻译,非人工翻译。准确性不能保证,请与英文版配合使用 ...

    Java应用安装包(eclipse+JDK_API+Java反编译)

    JDK(Java Development Kit)称为Java开发包或Java开发工具,是一个编写Java的Applet小程序和应用程序的程序开发环境。JDK是整个Java的核心,包括了Java运行环境(Java Runtime Envirnment),一些Java工具和Java的...

    java 官方原版jdk 1.6

    java 官方原版 jdk 1.6

    JavaCompiler --JDK6 API的简介(java动态编译)

    java动态编译的几种技术介绍。JavaCompiler --JDK6 API的简介(java动态编译)

    jdk7chm,java7帮助,jdk7api,jdk1.7帮且文档

    JDK是学好Java的第一步。而专门运行在x86平台的Jrocket在服务端运行效率也要比Sun JDK好很多。从SUN的JDK5.0开始,提供了泛型等非常实用的功能,其版本也不断更新,运行效率得到了非常大的提高。

    jdk6免安装下载 免费下载

    jdk6免安装下载 免费下载

    Java JDK 6学习笔记.zip

    Java JDK 6学习笔记.zipJava JDK 6学习笔记.zip Java JDK 6学习笔记.zip Java JDK 6学习笔记.zip

    实验一_JDK编程环境和Java基本语法.doc

    实验一_JDK编程环境和Java基本语法.doc 实验一_JDK编程环境和Java基本语法.doc 实验一_JDK编程环境和Java基本语法.doc

    java jdk6版本

    jdk6版本 很好用哦

Global site tag (gtag.js) - Google Analytics