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

Posted

技术标签:

【中文标题】访问最终局部变量是不是比 Java 中的类变量更快?【英文标题】:Is it faster to access final local variables than class variables in Java?访问最终局部变量是否比 Java 中的类变量更快? 【发布时间】:2011-09-29 23:35:10 【问题描述】:

我一直在研究一些 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 会有什么不同。

注意:这似乎类似于 this question,但这是针对 c++ 的,并没有说明为什么将它们声明为 final

【问题讨论】:

您可以尝试查看生成的 java 程序集以查看差异。 刚刚意识到原因可能在 HotSpot 编译器中,而不是字节码本身... 请发布您的基准测试代码,至少有一些机会您错误地对方法进行了基准测试并且实际上只测试了解释器而不是编译器:) 【参考方案1】:

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

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

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

iload 1  //N = 1

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

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

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

final 是另一回事。这基本上是对编译器/JIT 的一个提示,即此引用不会更改,因此它可以进行一些更重的优化。但这更难追踪,根据经验,尽可能使用final

【讨论】:

我想这个答案(尤其是最后一段)比标记的要好。 我想知道 final 中的一些加速是否可能是智能 JIT 可以知道在对象超出范围之前重用指针,并保存在 alloc() 上,并获得更好的缓存内存占用略小的命中... 完全同意。最有用的答案。【参考方案2】:

final 关键字在这里是一个红鲱鱼。 性能差异的出现是因为他们说的是两种不同的东西。

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

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

没有final boolean[] used,函数说“当索引小于当前对象的used字段的当前值的长度时,获取当前对象的used字段的当前值并对索引i处的元素做一些事情。”

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

【讨论】:

我很困惑你所说的final 是一个红鲱鱼。您的意思是访问变量不一定更快,但是 JIT 编译器可以优化循环以消除范围检查和查找? “即使忽略多个线程” - 只是为了说明这一点:JIT only 考虑线程本地行为。这意味着即使 used 是公共的(或者有一个 setter 方法)并且可能被另一个线程更改,JIT 完全有权忽略这一点。所以 JIT 真的只需要弄清楚 apply() 是否会改变引用(在实践中:如果它可以内联调用(和所有子调用)它会注意到它,否则你肯定不走运) 另外,“更快”行为的出现很有可能是因为有人再次编写了一个无效的 Java 基准测试(太容易做到,而且很难做到正确)——应该有一个性能解释器的差异,但如果 apply 非常简单,那么与现代 Hotspot 编译的代码确实不应该有任何区别 @job,我的意思是 JIT 编译器在局部推理方面比全局推理更好,所以字段是 final 并不像它是本地那么重要。我质疑“最终局部变量”更快的前提,并说使用“局部变量”比使用类实例成员更容易优化。 @Voo,即使p.apply 同步也是如此吗?【参考方案3】:

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

【讨论】:

不太对。局部变量放在线程stack上,而不是放在operand stack上。各种load/store 操作码用于将局部变量从堆栈移动到操作数堆栈并返回。见this image。【参考方案4】:

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

当然,随着 jit 变得越来越聪明并且可以自己解决这些问题,这些约定变得不那么有用了。

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

【讨论】:

嘿,我也在输入这个! :-) 除了我认为即使是编译器也可以从知道该方法对这些引用中的并行更改不感兴趣中受益。【参考方案5】:

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

这种手动调优对于更简单的 JVM 可能是值得的,例如安卓。

【讨论】:

dex (android) 字节码可能更有效...未压缩的 .dx 比 jar 压缩的 .class 小,dalvik 优于 java mobile 的全部原因是性能(标准 jvm 是对于移动设备来说太臃肿了)

以上是关于访问最终局部变量是不是比 Java 中的类变量更快?的主要内容,如果未能解决你的问题,请参考以下文章

JAVA中的final关键字

Java学习笔记23---内部类之局部内部类只能访问final的局部变量

Java中的类的field到底是指啥?

Java中的静态最终变量[重复]

Java:成员变量局部变量和静态变量

数据存储 --《高性能JavaScript》