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

我一直在研究一些java原始集合( trove , fastutil , hppc ),我注意到一个模式,类变量有时被声明为final局部变量。 例如:

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

我做了一些基准测试,看起来这样做的速度稍微快一点,但为什么会这样呢? 我试图理解如果函数的前三行被注释掉,Java将采取哪些不同的做法。

注意:这似乎与这个问题类似,但那是针对c ++的,并没有说明为什么它们被声明为final

final关键字是红鲱鱼。 性能差异之所以出现是因为他们说了两件不同的事情。

 public void forEach(IntIntProcedure p) { final boolean[] used = this.used; for (int i = 0; i < used.length; i++) { ... } } 

是说,“获取一个布尔数组,并为该数组的每个元素做一些事情。”

如果没有使用final boolean[] used ,函数会说“当索引小于当前对象的used字段的当前值的长度时,获取当前对象的used字段的当前值并used索引i元素。“

JIT可能更容易certificate循环绑定不变量以消除多余的绑定检查等等,因为它可以更容易地确定导致使用的值更改的原因。 即使忽略多个线程,如果p.apply可以更改p.apply的值,那么JIT也不能消除边界检查或进行其他有用的优化。

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

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

因此,当您访问局部变量时,您有一个固定偏移量的内存访问(N在编译时已知)。 这是用于访问第一个方法参数( int )的字节码:

 iload 1 //N = 1 

但是,当您访问字段时,实际上是在执行额外的步骤。 首先,您正在读取“ 局部变量 ”, this只是为了确定当前的对象地址。 然后你加载一个字段( getfield ),它有一个固定的偏移量。 因此,您执行两个内存操作而不是一个(或一个额外的)。 字节码:

 aload 0 //N = 0: this reference getfield total I //int total 

因此,技术上访问局部变量和参数比对象字段更快。 实际上,许多其他因素可能会影响性能(包括各种级别的CPU缓存和JVM优化)。

final是另一回事。 它基本上是编译器/ JIT的一个提示,这个引用不会改变,所以它可以做一些更重的优化。 但这很难追查,因为经验法则尽可能使用final

它告诉运行时(jit)在该方法调用的上下文中,这3个值永远不会改变,因此运行时不需要连续加载成员变量中的值。 这可能会略微提高速度。

当然,随着jit越来越聪明并且可以自己弄清楚这些东西,这些约定变得不那么有用了。

请注意,我没有明确表示加速比使用局部变量而不是最终部分更多。

在生成的VM操作码中,局部变量是操作数堆栈上的条目,而字段引用必须通过通过对象引用检索值的指令移动到堆栈。 我想JIT可以更容易地使堆栈引用寄存器引用。

这种简单的优化已经包含在JVM运行时中。 如果JVM对实例变量进行了天真的访问,那么我们的Java应用程序就会变慢。

这种手动调整对于更简单的JVM来说可能是值得的,例如Android。