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

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

public class ListExample { private Node head; private Node tail; private class Node { /* ... */ } public void append(T content) { if (!isEmpty()) { Node dummy = new Node(content); head = dummy; tail = dummy; head.setNext(head); // or this dummy.setNext(dummy); } else { /* ... */ } } // more methods // ... } 

我的问题是:对head.setNext(head)的调用是否会比dummy.setNext(dummy)慢? 即使它不明显。 我很想知道这个,因为head显然是类的实例变量而虚拟是本地的,所以本地访问会更快吗?

好的,我已经编写了一个微基准测试程序(由@Joni和@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访问更快 (即使两者都指向同一个对象)。

注意:我不想找到这个,因为我想要优化的东西,这只是纯粹的兴趣。

PS这是微基准的代码:

 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); } } 

通常,访问( this对象的)实例变量需要aload_0 (将其加载到堆栈的顶部),然后是getfield 。 引用局部变量只需要aload_n将值拉出堆栈中指定的位置。

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

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

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

就像在评论中一样,我不认为所花费的时间有所不同。 我认为你可能指的是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 循环 ,其中将访问各个value元素,它们将它从堆中带到堆栈( 局部变量 ),从而进行优化。

您还可以查看Jon Skeet,Vivin和其他几个人在此答案中分享的知识。

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

我认为使用dummy可能最多,1个周期更快,假设它留在寄存器中,但它取决于特定的CPU架构, setNext是什么样的,以及你正在使用的JVM,它真的是不可预测的代码在最终JITforms中的外观如何。 JVM可能会看到head == dummy,如果是这样,两个case的执行代码都是相同的。 这是一个非常小的问题。

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

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

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

 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."":(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。 在我看来他们会是相同的..