Java本地vs实例变量访问速度

时间:2014-02-06 20:14:51

标签: java performance variables optimization

所以我的问题是关于Java中的变量访问速度。今天在我的“CS”中(如果你可以称之为),老师提出了一个类似于以下列表的例子:

public class ListExample<T> {
    private Node<T> head;
    private Node<T> tail;

    private class Node<T> { /* ... */ }

    public void append(T content) {
        if (!isEmpty()) {
            Node<T> dummy = new Node<T>(content);
            head = dummy;
            tail = dummy;

            head.setNext(head);
            // or this
            dummy.setNext(dummy);

        } else { /* ... */ }
    }

    // more methods
    // ...
}

我的问题是:head.setNext(head)的电话会慢于dummy.setNext(dummy)吗?即使它不明显。 我想知道这一点,因为head很明显,类的实例var和dummy是本地的,所以本地访问会更快吗?

7 个答案:

答案 0 :(得分:19)

好的,我已经编写了一个微基准测试程序(由@Joni&amp; @MattBall建议),以下是每个本地变量和一个实例变量的1 x 1000000000次访问的结果:

Average time for instance variable access: 5.08E-4
Average time for local variable access: 4.96E-4

对于10 x 1000000000次访问:

Average time for instance variable access:4.723E-4
Average time for local variable access:4.631E-4

对于100 x 1000000000次访问:

Average time for instance variable access: 5.050300000000002E-4
Average time for local variable access: 5.002400000000001E-4

所以看起来局部变量访问确实比实例var访问更快(即使两者都指向同一个对象)。

注意:我不想发现这一点,因为我想要优化的东西,这只是纯粹的兴趣。

P.S。以下是微基准的代码:

public class AccessBenchmark {
    private final long N = 1000000000;
    private static final int M = 1;

    private LocalClass instanceVar;

    private class LocalClass {
        public void someFunc() {}
    }

    public double testInstanceVar() {
        // System.out.println("Running instance variable benchmark:");
        instanceVar = new LocalClass();

        long start = System.currentTimeMillis();
        for (int i = 0; i < N; i++) {
            instanceVar.someFunc();
        }

        long elapsed = System.currentTimeMillis() - start;

        double avg = (elapsed * 1000.0) / N;

        // System.out.println("elapsed time = " + elapsed + "ms");
        // System.out.println(avg + " microseconds per execution");

        return avg;
    }

    public double testLocalVar() {
        // System.out.println("Running local variable benchmark:");
        instanceVar = new LocalClass();
        LocalClass localVar = instanceVar;

        long start = System.currentTimeMillis();
        for (int i = 0 ; i < N; i++) {
            localVar.someFunc();
        }

        long elapsed = System.currentTimeMillis() - start;

        double avg = (elapsed * 1000.0) / N;

        // System.out.println("elapsed time = " + elapsed + "ms");
        // System.out.println(avg + " microseconds per execution");

        return avg;
    }

    public static void main(String[] args) {
        AccessBenchmark bench;

        double[] avgInstance = new double[M];
        double[] avgLocal = new double[M];

        for (int i = 0; i < M; i++) {
            bench = new AccessBenchmark();

            avgInstance[i] = bench.testInstanceVar();
            avgLocal[i] = bench.testLocalVar();

            System.gc();
        }

        double sumInstance = 0.0;
        for (double d : avgInstance) sumInstance += d;
        System.out.println("Average time for instance variable access: " + sumInstance / M);

        double sumLocal = 0.0;
        for (double d : avgLocal) sumLocal += d;
        System.out.println("Average time for local variable access: " + sumLocal / M);
    }
}

答案 1 :(得分:14)

通常,访问(this对象的)实例变量需要aload_0(将this加载到堆栈顶部),然后{{1} }。引用局部变量只需要getfield就可以将值从堆栈中指定的位置拉出来。

此外,aload_n必须引用类定义以确定值的存储位置(偏移量)。这可能是几个额外的硬件指令。

即使使用JITC,本地引用(通常为零/一个硬件操作)也不太可能比实例字段引用(必须至少一个操作,可能是2-3)慢。

(并不是说这一点非常重要 - 两者的速度都很好,差异只会在非常奇怪的情况下变得很重要。)

答案 2 :(得分:7)

在评论中,我认为所用的时间不一致。我认为你可能指的是Java SE代码库中更好的例证。例如,在java.lang.String

public void getBytes(int srcBegin, int srcEnd, byte dst[], int dstBegin) {
    //some code you can check out

    char[] val = value;
    while (i < n) {
        dst[j++] = (byte)val[i++];    /* avoid getfield opcode */
    }
}

在上面的代码中,value是一个实例变量,因为有一个while 循环,其中{{1}的各个元素要被访问,他们将它从堆中带到堆栈(局部变量),从而进行优化。

您还可以查看Jon Skeet,Vivin和其他少数人on this answer分享的知识。

答案 3 :(得分:4)

从微架构的角度来看,读取局部变量可能更便宜,因为它可能在寄存器中或至少在CPU缓存中。通常,读取实例变量可能导致昂贵的高速缓存未命中。在这种情况下,虽然变量刚刚写入,但它仍然可能在缓存中。您可以编写一个微基准来查找是否存在任何差异。

答案 4 :(得分:-1)

我认为使用dummy可能在非常最多,1个周期更快,假设它留在寄存器中,但它取决于具体的CPU架构,以及{{ 1}}看起来和你正在使用的JVM一样,代码在最终的JIT'd形式中看起来真是不可预测的。 JVM可能会看到head == dummy,如果是这样,两个case的执行代码都是相同的。这是一个非常小的问题。

答案 5 :(得分:-1)

我可以向你保证,无论从中获得什么性能提升,都会被看到令人困惑的代码编写的麻烦所抵消。让编译器搞清楚这一点。我承认一切都是平等的,局部变量可能稍快一些,只是因为涉及的字节码指令较少。但是,谁说未来版本的JVM不会改变这个?

简而言之,编写易于首先阅读的代码。在此之后,如果您有性能问题,请参阅个人资料。

答案 6 :(得分:-2)

如有疑问,请查看生成的字节码

public void append(java.lang.Object);
 Code:
  0:    new #2; //class ListExample$Node
  3:    dup
  4:    aload_0
  5:    aload_1
  6:    invokespecial   #3; //Method ListExample$Node."<init>":(LListExample;Ljava/lang/Object;)V
  9:    astore_2
  10:   aload_0
  11:   aload_2
  12:   putfield    #4; //Field head:LListExample$Node;
  15:   aload_0
  16:   aload_2
  17:   putfield    #5; //Field tail:LListExample$Node;
  20:   aload_0
  21:   getfield    #4; //Field head:LListExample$Node;
  24:   aload_0
  25:   getfield    #4; //Field head:LListExample$Node;
  28:   invokevirtual   #6; //Method ListExample$Node.setNext:(LListExample$Node;)V
  31:   aload_2
  32:   aload_2
  33:   invokevirtual   #6; //Method ListExample$Node.setNext:(LListExample$Node;)V
  36:   return

}

要么得到aload,接着是getfield或2 x aload。在我看来他们会是相同的..