访问最终局部变量比Java中的类变量更快吗?

时间:2011-07-06 21:01:53

标签: java optimization micro-optimization

我一直在研究一些java原始集合(trovefastutilhppc),我注意到一种模式,即类变量有时被声明为{ {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]); } } }

5 个答案:

答案 0 :(得分:26)

访问局部变量或参数是一步操作:获取位于堆栈上偏移量N的变量。如果你的函数有2个参数(简化):

  • N = 0 - this
  • N = 1 - 第一个参数
  • N = 2 - 第二个参数
  • N = 3 - 第一个局部变量
  • N = 4 - 第二个局部变量
  • ...

因此,当您访问局部变量时,您有一个固定偏移量的内存访问(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可能是值得的,例如,机器人。