我一直在研究一些java原始集合(trove,fastutil,hppc),我注意到一种模式,即类变量有时被声明为{ {1}}局部变量。例如:
final
我做了一些基准测试,看起来这样做时略微更快,但为什么会这样呢?我试图理解如果函数的前三行被注释掉,Java会做些什么不同。
注意:这似乎与this question类似,但那是针对c ++的,并没有说明为什么声明public void forEach(IntIntProcedure p) {
final boolean[] used = this.used;
final int[] key = this.key;
final int[] value = this.value;
for (int i = 0; i < used.length; i++) {
if (used[i]) {
p.apply(key[i],value[i]);
}
}
}
。
答案 0 :(得分:26)
访问局部变量或参数是一步操作:获取位于堆栈上偏移量N的变量。如果你的函数有2个参数(简化):
this
因此,当您访问局部变量时,您有一个固定偏移量的内存访问(N在编译时已知)。这是用于访问第一个方法参数(int
)的字节码:
iload 1 //N = 1
但是,当您访问字段时,实际上是在执行额外的步骤。首先,您只是为了确定当前对象地址而阅读“局部变量”this
。然后,您正在加载一个与getfield
具有固定偏移量的字段(this
)。因此,您执行两个内存操作而不是一个(或一个额外的)。字节码:
aload 0 //N = 0: this reference
getfield total I //int total
因此技术上访问局部变量和参数比对象字段更快。实际上,许多其他因素可能会影响性能(包括各种级别的CPU缓存和JVM优化)。
final
是另一回事。它基本上是编译器/ JIT的一个提示,这个引用不会改变,所以它可以做一些更重的优化。但这很难追踪,因为尽可能使用final
。{/ p>
答案 1 :(得分:8)
final
关键字在这里是一个红色的鲱鱼。
性能差异是因为他们说了两件不同的事情。
public void forEach(IntIntProcedure p) {
final boolean[] used = this.used;
for (int i = 0; i < used.length; i++) {
...
}
}
说,“获取一个布尔数组,并为的每个元素数组执行某些操作。”
如果没有final boolean[] used
,函数会说“当索引小于当前对象的used
字段当前值的长度时,获取{{1}的当前值当前对象的字段,并使用索引used
处的元素执行某些操作。“
JIT可能更容易证明循环绑定不变量以消除多余的绑定检查等等,因为它可以更容易地确定导致i
值变化的原因。即使忽略多个线程,如果used
可以更改p.apply
的值,那么JIT也无法消除边界检查或进行其他有用的优化。
答案 2 :(得分:2)
它告诉运行时(jit)在该方法调用的上下文中,这3个值永远不会改变,因此运行时不需要连续加载成员变量中的值。这可能会略微提高速度。
当然,随着jit越来越智能并且能够自行解决这些问题,这些约定变得不那么有用了。
请注意,我并没有说明加速比使用局部变量更多的是最终部分。
答案 3 :(得分:1)
在生成的VM操作码中,局部变量是操作数堆栈上的条目,而字段引用必须通过通过对象引用检索值的指令移动到堆栈。我想JIT可以更容易地使堆栈引用寄存器引用。
答案 4 :(得分:0)
这种简单的优化已经包含在JVM运行时中。如果JVM对实例变量进行了简单的访问,那么我们的Java应用程序就会变慢。
这种手动调整对于更简单的JVM可能是值得的,例如,机器人。