静态和非静态字段之间算术运算的性能差异
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
进行基准测试,看看循环去了哪里以及生成了哪些汇编代码。
狩猎愉快!
我有一个计算事件的 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
进行基准测试,看看循环去了哪里以及生成了哪些汇编代码。
狩猎愉快!