Java反射:方法调用比字段快?

4

我正在进行一些代码微基准测试(请友善对待),并遇到了这个谜题:使用反射读取字段时,调用getter方法比读取Field更快。

简单的测试类:

private static final class Foo {
    public Foo(double val) {
        this.val = val;
    }
    public double getVal() { return val; }
    public final double val; // only public for demo purposes
}

我们有两个反思:
Method m = Foo.class.getDeclaredMethod("getVal", null);
Field  f = Foo.class.getDeclaredField("val");

现在我在循环中调用两个反射方法,对于Method执行invoke,对于Field执行get。第一次运行是为了热身虚拟机,第二次运行进行10M次迭代。方法调用始终快30%,但为什么?请注意,在循环中不会调用getDeclaredMethod和getDeclaredField。它们在循环中只调用一次并在同一对象上执行。
我还尝试了一些小变化:使字段非final,可传递,非公共等。所有这些组合都导致统计上相似的性能。
编辑:这是在WinXP,Intel Core2 Duo,Sun JavaSE build 1.6.0_16-b01下运行的,使用jUnit4和Eclipse。
3个回答

3
我的猜测是getDeclaredField和getDeclaredMethod的实现方式有所不同:每次调用getDeclaredField时,它都必须检查变量类型和大小,以返回一个实际的对象或原始类型;而getDeclaredMethod将返回指向同一方法的指针,该方法静态地处理所有其余部分。 编辑: 我的解释类似:对于每个类,方法仅包含在内存中一次,而每个对象实例可以具有不同的属性值。当您通过执行方法调用来获取属性值(仍然只使用方法指针)时,编译器已经优化了方法以访问参数,知道确切的类层次结构等;而当您使用“get”来获取属性的值时,您让反射执行getter方法的工作,显然没有编译器优化。

我已经在问题中添加了澄清,这些方法只被调用一次,在循环外部。循环仅包含反射调用。 - omerkudat
谢谢你澄清这个问题。我已经相应地编辑了我的答案。 - weltraumpirat
对象的内存布局对于每个实例不都是相同的吗?我想这可能会导致一种优化,其中字段从对象地址读取特定偏移量。请注意,该类是final的,因此子类布局不会干扰。 - omerkudat
是的,确切地说:内存布局将是相同的 - 但当您使用反射时,编译器无法知道这一点。当您只需要执行这样一个简单的任务时,两个或三个操作之间的差异已经占据了30%以上的速度...我恐怕不了解足够有关javac的知识来给出更详细的答案,但我认为这个理论可以证明一个有根据的猜测。;) - weltraumpirat

1
在您的微基准测试中,方法调用之所以更快,是由于JVM / Hotspot在您的循环中进行了优化。
更改您的微基准测试:
创建一个循环,在该循环中:通过反射读取值,然后增加1(例如),然后通过反射将其分配给同一字段。在循环之外,进行最终读取并System.out.println它...
执行这两个变体(字段vs方法),您会发现真正的区别正好相反:方法调用实际上比字段慢30-40%。
问候

0

这是否意味着double d = Foo.getVal()double d = Foo.val快30%?


1
绝对不是。而且反射比简单的赋值要慢得多。 - Andreas Dolk
我希望这不是事实,因为我可以想象人们会急于用公开字段替换它们的getter,我们都知道那将是一件糟糕的事情。 - omerkudat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接