`
RednaxelaFX
  • 浏览: 3052794 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论
阅读更多
今天有朋友提到一个叫 ReflectASM的库,为Java环境提供高性能的反射操作支持。它的实现方式是动态代码生成。
以前我的一篇日志里写过,Oracle/Sun JDK6的反射方法调用的实现当中重要的一环就是动态代码生成。

但是今天的主角不是ReflectASM,而是Oracle/Sun JDK里的sun.misc.Unsafe类,以及这个类上的getInt(Object, long)方法:
引用
/**
 * Fetches a value from a given Java variable.
 * More specifically, fetches a field or array element within the given
 * object <code>o</code> at the given offset, or (if <code>o</code> is
 * null) from the memory address whose numerical value is the given
 * offset.
 * <p>
 * The results are undefined unless one of the following cases is true:
 * <ul>
 * <li>The offset was obtained from {@link #objectFieldOffset} on
 * the {@link java.lang.reflect.Field} of some Java field and the object
 * referred to by <code>o</code> is of a class compatible with that
 * field's class.
 *
 * <li>The offset and object reference <code>o</code> (either null or
 * non-null) were both obtained via {@link #staticFieldOffset}
 * and {@link #staticFieldBase} (respectively) from the
 * reflective {@link Field} representation of some Java field.
 *
 * <li>The object referred to by <code>o</code> is an array, and the offset
 * is an integer of the form <code>B+N*S</code>, where <code>N</code> is
 * a valid index into the array, and <code>B</code> and <code>S</code> are
 * the values obtained by {@link #arrayBaseOffset} and {@link
 * #arrayIndexScale} (respectively) from the array's class.  The value
 * referred to is the <code>N</code><em>th</em> element of the array.
 *
 * </ul>
 * <p>
 * If one of the above cases is true, the call references a specific Java
 * variable (field or array element).  However, the results are undefined
 * if that variable is not in fact of the type returned by this method.
 * <p>
 * This method refers to a variable by means of two parameters, and so
 * it provides (in effect) a <em>double-register</em> addressing mode
 * for Java variables.  When the object reference is null, this method
 * uses its offset as an absolute address.  This is similar in operation
 * to methods such as {@link #getInt(long)}, which provide (in effect) a
 * <em>single-register</em> addressing mode for non-Java variables.
 * However, because Java variables may have a different layout in memory
 * from non-Java variables, programmers should not assume that these
 * two addressing modes are ever equivalent.  Also, programmers should
 * remember that offsets from the double-register addressing mode cannot
 * be portably confused with longs used in the single-register addressing
 * mode.
 *
 * @param o Java heap object in which the variable resides, if any, else
 *        null
 * @param offset indication of where the variable resides in a Java heap
 *        object, if any, else a memory address locating the variable
 *        statically
 * @return the value fetched from the indicated Java variable
 * @throws RuntimeException No defined exceptions are thrown, not even
 *         {@link NullPointerException}
 */
public native int getInt(Object o, long offset);

(注意:sun.misc.Unsafe是Oracle/Sun JDK里的私有实现,不是Java标准库的共有API,请权衡好利害关系后再使用。)

朋友利用Unsafe.getInt()来实现了快速的反射访问字段的功能,写了这么篇日志:
Java 反射调用的一种优化
在文章末尾,他提到直接用Unsafe.getInt()与用普通反射访问字段的性能对比,
aliveTimeID 写道
通过测试发现,效率是普通java.lang.reflect.Field.get(Object)的3倍,当然,性能这个东西,还是自己测试了放心。


于是问题就来了:在我的机器上也是快三倍么?为什么“快了三倍”?是什么跟什么比较快了三倍?

把原本的测试代码拿来:(有细微改动)
import java.io.Serializable;
import java.lang.reflect.Field;
import sun.misc.Unsafe;
/**
 * @author haitao.yao Dec 14, 2010
 */
public class ReflectionCompare {
  private static final int count = 10000000;
  /**
   * @param args
   */
  public static void main(String[] args) {
    long duration = testIntCommon();
    System.out.println("int common test for  " + count
        + " times, duration: " + duration);
    duration = testUnsafe();
    System.out.println("int unsafe test for  " + count
        + " times, duration: " + duration);
  }
  private static long testUnsafe() {
    long start = System.currentTimeMillis();
    sun.misc.Unsafe unsafe = getUnsafe();
    int temp = count;
    Field field = getIntField();
    long offset = unsafe.objectFieldOffset(field);
    while (temp-- > 0) {
      unsafe.getInt(new TestBean(), offset);
    }
    return System.currentTimeMillis() - start;
  }
  private static long testIntCommon() {
    long start = System.currentTimeMillis();
    int temp = count;
    getIntField().setAccessible(true);
    try {
      while (temp-- > 0) {
        TestBean bean = new TestBean();
        getIntField().get(bean);
      }
      return System.currentTimeMillis() - start;
    } catch (Exception e) {
      e.printStackTrace();
    }
    return -1;
  }
  private static final sun.misc.Unsafe unsafe;
  static {
    sun.misc.Unsafe value = null;
    try {
      Class<?> clazz = Class.forName("sun.misc.Unsafe");
      Field field = clazz.getDeclaredField("theUnsafe");
      field.setAccessible(true);
      value = (Unsafe) field.get(null);
    } catch (Exception e) {
      e.printStackTrace();
      throw new RuntimeException("error to get theUnsafe", e);
    }
    unsafe = value;
  }
  public static final sun.misc.Unsafe getUnsafe() {
    return unsafe;
  }
  private static final Field intField;
  private static final Field stringField;
  static {
    try {
      intField = TestBean.class.getDeclaredField("age");
      stringField = TestBean.class.getDeclaredField("name");
    } catch (Exception e) {
      e.printStackTrace();
      throw new IllegalStateException("failed to init testbean field", e);
    }
  }
  public static final Field getIntField() {
    return intField;
  }
  public static final Field getStringField() {
    return stringField;
  }
  
  /**
   * @author haitao.yao
   * Dec 14, 2010
   */
  static class TestBean implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = -5994966479456252766L;
    
    private String name;
    private int age;
    /**
     * @return the name
     */
    public String getName() {
      return name;
    }
    /**
     * @param name the name to set
     */
    public void setName(String name) {
      this.name = name;
    }
    /**
     * @return the age
     */
    public int getAge() {
      return age;
    }
    /**
     * @param age the age to set
     */
    public void setAge(int age) {
      this.age = age;
    }
  }
}

在我的测试环境里跑:
$ java -version
java version "1.6.0_25"
Java(TM) SE Runtime Environment (build 1.6.0_25-b06)
Java HotSpot(TM) 64-Bit Server VM (build 20.0-b11, mixed mode)
$ java -cp . ReflectionCompare
int common test for  10000000 times, duration: 616
int unsafe test for  10000000 times, duration: 10
$ java -cp . ReflectionCompare
int common test for  10000000 times, duration: 425
int unsafe test for  10000000 times, duration: 274
$ java -cp . ReflectionCompare
int common test for  10000000 times, duration: 502
int unsafe test for  10000000 times, duration: 10
$ java -cp . ReflectionCompare
int common test for  10000000 times, duration: 659
int unsafe test for  10000000 times, duration: 9
$ java -cp . ReflectionCompare
int common test for  10000000 times, duration: 646
int unsafe test for  10000000 times, duration: 10


这时间看起来不像是“三倍”,而更像是哪里出岔子了

那么在揭开谜底前,我们来从侧面看看能不能有啥发现。

我们已经知道上面对比的双方是Unsafe.getInt()和Field.get()。Unsafe.getInt()是个native方法,就算是到头了。
实际上Unsafe.getInt()方法在HotSpot VM里是个intrinsic方法,有特别的优化处理。欲知详情可以参考HotSpot server compiler的LibraryCallKit::inline_unsafe_access()函数,它可以将原本的Unsafe.getInt()方法调用内联到调用点上,彻底削除JNI函数调用开销。
但想必这点优化没啥影响,大家是公平的?后面再继续说。

在Oracle/Sun JDK6里,Field.get()又是怎样实现的呢?
跟随FieldReflectionFactoryUnsafeFieldAccessorFactoryUnsafeFieldAccessorImplUnsafeIntegerFieldAccessorImpl这些类的相关方法的实现,可以发现当我们要通过反射获取一个int类型的字段时,最终在UnsafeIntegerFieldAccessorImpl也是通过Unsafe.getInt()来干活的:
public int getInt(Object obj) throws IllegalArgumentException {
    ensureObj(obj);
    return unsafe.getInt(obj, fieldOffset);
}

也就是说,Oracle/Sun JDK6虽然用动态生成代码的方式来实现反射调用方法,但在反射访问字段上却选择了不动态生成代码而直接使用Unsafe这样的native后门。

那么,自己直接使用Unsafe.getInt()比Field.get()占优的地方就在于前者很直接而后者经过了若干层间接/包装,所以前者比后者快。这足以上面看到的测试时间的差异么?

现在让我们来看看事情的真相。
换用Oracle JDK 6 update 25 build 03 fastdebug版,通过-XX:+PrintOptoAssembly来观察JIT编译器生成出来的代码的样子:
$ ~/jdk/6u25b03_x64_debug/fastdebug/bin/java -cp . -XX:+PrintOptoAssembly ReflectionCompare

可以看到测试里的ReflectionCompare.testUnsafe()方法中的主要计时循环生成为这样的代码:
060   B6: #     B6 B7 <- B5 B6  Loop: B6-B6 inner stride: not constant  Freq: 999998
060   
060   
060     decl    RBP     # int
062     cmpl    RBP, #-1
065     jg,s   B6       # loop end  P=1.000000 C=12203.000000

换回等价的Java代码来表达,也就是:
while (temp-- > 0) {
}

但原本测试代码里写的是:
while (temp-- > 0) {
  unsafe.getInt(new TestBean(), offset);
}


而这正是本篇日志的标题所说的:别测空循环
连被测的对象都被优化掉了的话,这种microbenchmark就彻底没意义了。
前面已经提到,Unsafe.getInt()是HotSpot VM的一个intrinsic方法,因而它的所有特性都是VM清楚了解的,包括有没有副作用、是否能安全的内联、内联后是否可以进一步优化直至完全削除,等。
所以说不要小看了某个native方法的优化对microbenchmark的影响。

用同样办法观察原本测试里的ReflectionCompare.testIntCommon()方法,能看到整个调用过程,包括unsafe.getInt()被内联后的逻辑,都还完好的存在于JIT编译生成的代码中。
在这种条件下比较两边的性能,自然是毫无参考价值可言。

当然,我们还是能得到一些收获。
这个测试里,两边最终干活的都是Unsafe.getInt(),但直接调就优化掉了,而通过Field.get()来调就没被优化掉。
这正好体现了通用型API的多层繁复的封装带来的问题——过多的封装引来了过多间接层,间接层多了之后编译器要优化就会很吃力。
想像一下,如果有人发现直接调用Unsafe.getInt()方法字段的速度很快,然后好心的想把它封装成通用的反射库提供给大家用,很可能发生的状况就是他又把间接层堆积了起来,结果最终用户用的时候就跟直接用反射差不多。

话说回来,一般程序员写Java程序的时候也无需为这种问题纠结就是了。这篇故事看了笑笑就好
分享到:
评论
1 楼 lydawen 2011-11-02  
这么个‘小问题’就研究这么深入,真是有心了。如果循环中新的实例需要在循环个引用,如加入到数组,结果应该比较真实

相关推荐

    CRC循环冗余检测

    最后,将余数拼到信息码左移后空出的位置,得到完整的 CRC 码。 例如,数据码 M=101001,生成码 P=1101,则信息码多项式为 K(x)=x5+x3+1,生成多项式为 G(X)=X8+X6+X3。模 2 除法运算结果为 R=001,则 CRC 码为 ...

    《检查处理kettle数据流中的空行》示例附件代码

    我们可以设置规则,例如检查行是否为空,即所有字段都没有值。如果满足条件,该行将被排除,不传递到下一个步骤。 4. **条件设置**:在"Filter Rows"步骤中,我们需要配置条件表达式。对于空行,我们可以检查每列的...

    循环码_循环码编译码实现_

    循环码是一种特殊的纠错编码技术,广泛应用于数据通信、存储系统和深空探测等领域。它的核心原理是通过在原始数据中添加冗余位,使得整个编码序列具有循环特性,即任意长度的子序列首尾相连都能形成一个合法的编码。...

    采购与付款-采购与付款业务循环符合性测试程序表.doc

    采购与付款业务循环符合性测试程序表 本文档介绍了采购与付款业务循环符合性测试程序表的知识点,涵盖了采购、验收、发票传递、付款和会计处理等方面。 一、采购部分 * Auditing the segregation of duties ...

    减少Linux内核空循环 降低系统能耗技巧.docx

    减少Linux内核空循环是优化系统能耗的关键策略之一,尤其对于依赖电池电源的便携式设备如笔记本电脑和服务器来说,降低能耗不仅意味着更长的电池寿命,还能够减少运行成本。Linux内核的"tickless"技术正是针对这一...

    数据结构之循环顺序队列

    循环顺序队列是对普通顺序队列的改进,解决了顺序队列在满时无法继续插入元素,空时无法继续删除元素的问题。 循环顺序队列是一种线性结构,它在内存中连续分配空间,并通过指针来模拟队列的“首”和“尾”。在循环...

    循环码编译码

    12. **应用场景**:循环码广泛应用于移动通信、卫星通信、深空探测、数据存储系统以及计算机网络中,以确保数据传输的准确性。 13. **优化与改进**:随着技术的发展,人们不断探索新的编码理论和方法,如低密度奇偶...

    labview——while循环

    - **循环控制**:你可以在循环体内修改控制循环的布尔值,实现循环的提前退出或无限循环。 - **结束条件**:需要预先设定一个或多个条件,当这些条件满足时,改变循环头的布尔值,使循环停止。 在提供的例子“while...

    C#循环队列

    循环队列是一种线性数据结构,它在计算机科学中被广泛应用于实现各种算法,如缓存、消息队列等。在C#编程语言中,我们可以使用内置的数据结构或自定义结构来实现循环队列。本篇文章将深入探讨C#中的循环队列,包括其...

    黑盒测试和白盒测试区别及测试案例

    - 在循环边界条件下测试循环体的正确性。 - 检查内部数据结构的有效性。 ##### 3. 测试用例设计方法 - **路径覆盖**:确保软件的所有可能路径都被测试到。 - **分支覆盖**:测试所有逻辑判断的真/假两种结果。 - **...

    电子功用-用于增强燃料表空电压预测的负峰值电压检测

    为了符合环保法规,现代汽车配备了碳罐控制系统,该系统捕获并再循环这些燃油蒸汽,以减少排放。在这个过程中,涉及到的电气系统必须维持稳定的电压,确保系统的正常运行。 负峰值电压检测有助于预测可能出现的电压...

    QTC 其他流程循环-空白底稿.zip

    在IT行业中,流程循环是软件开发过程中的一个重要环节,它涉及到项目管理、质量控制和持续改进等多个方面。QTC(Quality Assurance and Testing Cycle)是一种常见的质量保证和测试流程,旨在确保产品的质量和性能...

    循环链表 数据结构

    循环链表的测试主要涉及验证上述基本操作(插入、删除、创建、输出)的正确性,以及检查链表在各种边界条件下的行为,如空链表、只有一个元素的链表等情况。此外,还需要测试链表在高负载或异常情况下的稳定性和性能...

    一个串口循环队列,可以制作协议收发,非常好用

    在常规队列满或空的情况下,循环队列能避免这些问题,因为它可以在队尾出队后立即在原位置入队,从而提高效率。在串口通信中,循环队列常用于缓冲数据,确保数据的连续性和一致性,尤其是在数据量大或者接收速率快的...

    Android之循环队列操作

    循环队列是一种常见的线性数据结构,它具有先进先出(FIFO)的特性,即最早进入队列的元素最先离开。这个特性使得循环队列在处理一系列有序任务或者缓存管理等方面有着广泛的应用。本文将详细探讨Android中如何实现...

    c++实现的循环链表

    循环链表是一种特殊类型的链表,它在最后一个元素之后连接回第一个元素,形成一个闭合的环状结构。这种数据结构在处理具有周期性或循环性质的问题时特别有用。在这个项目中,我们有两个源文件:`CirLL.cpp` 和 `main...

    非循环单链表的C++实现

    `initialize()`函数用于创建空链表,设置头指针为nullptr。 `insert()`函数接收一个值,创建新节点并将其插入到链表的适当位置。插入操作可能发生在链表头部、尾部或中间,具体取决于插入规则。 `remove()`函数...

    循环码编译码通用版

    循环码在许多实际应用中都有所体现,如CD-ROM、DVD、卫星通信、深空探测和无线网络等。在这些领域,循环码能提高数据传输的可靠性,确保数据在经过噪声通道后仍能准确恢复。 总结,本项目提供了一个用C语言实现的...

    基于java的循环双链表

    循环双链表是一种数据结构,它在单链表的基础上增加了前后指针,使得链表的首尾相连,形成一个环状。在Java编程中,我们可以使用面向对象的设计思想来实现循环双链表。通常,这样的数据结构会包含三个主要部分:一个...

Global site tag (gtag.js) - Google Analytics