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

Performance difference in arithmetic operations between static and non-static field

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

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();
       ...
    }
    ...
}

如果我从字段中删除 static 修饰符(打算使其成为 class 参数),计数吞吐量 比 [=] 降低 更多32=]25% 根据 JMH 报告。

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

JVM 将静态最终字段优化为真正的常量,但它不会对实例字段做同样的事情。理论上,可以对代码进行分析和证明,以表明该字段始终相同,但更复杂。此外,由于反射后门,final 字段不会被视为真正的 final。有一个 Jira 项目可以跟踪这个问题,但我现在找不到它。在内部,JDK 使用特殊的 @Stable 注释来优化对最终实例字段的访问。

但即使您可以使用此注释,仍然需要额外的分析来证明该字段对于所有实例都是相同的。在大多数情况下,分配字段的代码需要完全内联才能进行分析。如果将 Duration.ofMillis 调用实现为 return 一个随机数会怎样?当然不是,但是没有分析,编译器怎么可能确定呢?

这里有 2 个优化:

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

为了进一步研究这个问题,我建议您 运行 使用 perfasm 进行基准测试,看看循环去了哪里以及生成了哪些汇编代码。

狩猎愉快!