所以我的问题是关于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是本地的,所以本地访问会更快吗?
答案 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。在我看来他们会是相同的..