jvm 静态和非静态字段之间算术运算的性能差异

xbp102n0  于 2022-11-23  发布在  其他
关注(0)|答案(2)|浏览(116)

我有一个计算事件的类。它看起来像这样:

public class Counter {
    private static final long BUCKET_SIZE_NS = Duration.ofMillis(100).toNanos();
    ...

    private long nextBucketNum() {
        return clock.getTime() / BUCKET_SIZE_NS;
    }

    public void count() {
       ...
       final long num = nextBucketNum();
       ...
    }
    ...
}

根据JMH报告,如果我从字段中删除static修饰符(打算使其成为类参数),计数吞吐量 * 下降 * 超过25%
static用例生成的字节码:

INVOKEINTERFACE Clock.getTime ()J (itf)
 GETSTATIC Counter.BUCKET_SIZE_NS : J
 LDIV

对于non-static,则为:

INVOKEINTERFACE Clock.getTime ()J (itf)
ALOAD 0
GETFIELD Counter.BUCKET_SIZE_NS : J
LDIV

我是否在执行性能测试时遇到了某种类型的死代码消除问题,或者是某种级别的合理微优化(如JIT超线程)?
在单线程和多线程性能指标评测中都存在差异。
使用环境:

JMH version: 1.34
VM version: JDK 1.8.0_161, Java HotSpot(TM) 64-Bit Server VM, 25.161-b12

macOS Monterey 12.2.1

Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
kiz8lqtg

kiz8lqtg1#

这里有两个优化:

  • 恒定折叠:静态final字段是预先计算的,并写入代码blob(JIT编译的最终结果)。与内存负载(读取字段时)相比,这将转化为性能优势。
  • 算术简化:当除以一个潜在的变量时,编译器必须使用一个除法指令,这是非常昂贵的。当除以一个常数时,编译器可以想出一个更便宜的替代方法。这在除以(和乘以)2的幂时尤其如此,这可以简化为移位指令。

为了进一步了解这一点,我建议您使用perfasm运行基准测试,看看循环进行到哪里,生成了什么汇编代码。
狩猎愉快!

bakd9h0s

bakd9h0s2#

JVM将静态final字段优化为真正的常量,但它不会对示例字段执行相同的操作。理论上,代码可以被分析和证明,以显示字段总是相同的,但这更复杂。此外,final字段不会被视为真正的final,因为反射后门。有一个Jira项目跟踪这个问题,但我现在找不到它。在内部,JDK使用特殊的@Stable注解来优化对最终示例字段的访问。
但是,即使您可以使用这个注解,仍然需要额外的分析来证明该字段对于所有示例都是相同的。在大多数情况下,分配字段的代码需要完全内联才能进行分析。如果Duration.ofMillis调用被实现为返回一个随机数呢?当然不是,但是没有分析,编译器怎么能确定呢?

相关问题