为什么方法访问似乎比字段访问更快?

6

我正在进行一些测试,以了解使用getter/setter和直接字段访问之间的速度差异。我编写了一个简单的基准应用程序:

public class FieldTest {

    private int value = 0;

    public void setValue(int value) {
        this.value = value;
    }

    public int getValue() {
        return this.value;
    }

    public static void doTest(int num) {
        FieldTest f = new FieldTest();

        // test direct field access
        long start1 = System.nanoTime();

        for (int i = 0; i < num; i++) {
            f.value = f.value + 1;
        }
        f.value = 0;

        long diff1 = System.nanoTime() - start1;

        // test method field access
        long start2 = System.nanoTime();

        for (int i = 0; i < num; i++) {
            f.setValue(f.getValue() + 1);
        }
        f.setValue(0);

        long diff2 = System.nanoTime() - start2;

        // print results
        System.out.printf("Field Access:  %d ns\n", diff1);
        System.out.printf("Method Access: %d ns\n", diff2);
        System.out.println();
    }

    public static void main(String[] args) throws InterruptedException {
        int num = 2147483647;

        // wait for the VM to warm up
        Thread.sleep(1000);

        for (int i = 0; i < 10; i++) {
            doTest(num);
        }
    }

}

每次我运行它时,都会得到如下一致的结果:http://pastebin.com/hcAtjVCL 我想知道为什么字段访问似乎比getter / setter方法访问要慢,以及为什么最后8个迭代执行速度非常快。
编辑:考虑到“assylias”和“Stephen C”的意见,我已更改了代码,链接为http://pastebin.com/Vzb8hGdc,在那里我获得了略微不同的结果:http://pastebin.com/wxiDdRix

2
我认为编译器会自动内联getter方法。你是否检查过字节码? - jlordo
10
“/* 等待虚拟机预热 */ Thread.sleep(1000);” - 这并不起作用。如果什么也没做,JVM不会预热... 你的微基准测试存在几个缺陷,尤其是:(i)你没有真正让JVM预热(ii)你想要测试的两个路径在同一个方法中,这可能会阻止一些优化。 - assylias
11
@jlordo - 内联是由JIT编译器完成的。在字节码中不会显现出来。 - Stephen C
1
我已经检查了字节码(http://pastebin.com/Qqg4DbyN),似乎编译器没有将其内联。然而,正如Stephen C所说,它被JIT编译器内联了。 - Tom Leese
实际上,从时间上看,整个循环似乎已经被优化掉了... - assylias
1个回答

9
解释是你的基准测试有问题。
第一次迭代使用解释器完成。
Field Access:  1528500478 ns
Method Access: 1521365905 ns

第二次迭代首先由解释器完成,然后我们切换到运行JIT编译的代码。
Field Access:  1550385619 ns
Method Access: 47761359 ns

剩余的迭代都使用JIT编译代码完成。
Field Access:  68 ns
Method Access: 33 ns

etcetera

他们之所以运行得如此之快,是因为JIT编译器已经针对循环进行了优化。它检测到这些循环在计算中没有任何有用的贡献。至于第一个数字似乎比第二个数字始终更快,尽管优化后的代码未能以任何有意义的方式衡量字段与方法的访问。
更新代码/结果:显然JIT编译器仍在对循环进行优化。

那么,@Stephen使用访问器方法不仅有利于封装,还有助于提高速度??听起来太棒了!!! - Victor
@Victor - 我不认为你可以得出那个结论。实际上,我不认为你可以从这个基准测试中得出任何结论。它只是有缺陷的。 - Stephen C
谢谢Stephen!我明白了...我开始也有这样的想法。所以,如果你追求速度,使用直接访问;如果你想要更灵活的设计,使用访问器方法?(抱歉,问题有点跑题!) - Victor
1
我认为你也不能得出那个结论。我的理解是,一个好的JIT编译器会内联一个简单的getter或setter,使它们执行与直接字段访问/更新相同。 - Stephen C

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