我正在尝试以下代码进行性能检查。它纯粹是 CPU 密集型的,对类型进行大量计算double
,单线程,并且不使用任何堆:
public class PerfTestSampleJ {
private static final int MEASURE_COUNT = 5;
private static final int ITERATIONS = 100_000_000;
public static void main(String[] args) {
var minTime = Long.MAX_VALUE;
for (int i = 1; i <= MEASURE_COUNT; i++) {
long start = System.nanoTime();
double pi = calculatePi(ITERATIONS);
long time = System.nanoTime() - start;
System.out.printf("Iteration %2d took %8.3f ms%n", i, time / 1e6);
if (time < minTime) {
minTime = time;
}
if (Math.abs(pi - Math.PI) > 1e-14)
throw new AssertionError(pi + " (" + (pi - Math.PI) + ")");
}
System.out.printf("Minimum time taken: %8.3f ms%n", minTime / 1e6);
}
private static double calculatePi(int iterations) {
double pi = 0.0;
double numerator = 4.0;
for (int i = 1; i <= iterations; i++) {
double n = i * 2.0;
double denominator = n * (n + 1) * (n + 2);
pi += numerator / denominator;
numerator = -numerator;
}
return 3 + pi;
}
}
现在,使用相同的编译类文件,比较在 JRE 21 和 JRE 23 下运行的结果:
/usr/lib/jvm/jdk-21.0.5-oracle-x64/bin/java PerfTestSampleJ
Iteration 1 took 801.058 ms
Iteration 2 took 798.392 ms
Iteration 3 took 414.688 ms
Iteration 4 took 413.959 ms
Iteration 5 took 416.867 ms
Minimum time taken: 413.959 ms
/usr/lib/jvm/jdk-23.0.1-oracle-x64/bin/java PerfTestSampleJ
Iteration 1 took 193.654 ms
Iteration 2 took 186.790 ms
Iteration 3 took 102.963 ms
Iteration 4 took 103.226 ms
Iteration 5 took 102.869 ms
Minimum time taken: 102.869 ms
在每次运行中,前两次迭代都有一个预热阶段,但第三次迭代及以后的速度几乎是最快的。
Java 23 中有哪些变化可以加快速度?查看发行说明时,我发现有关性能的所有信息都是垃圾收集器的改进。但我们这里不使用堆,因此垃圾收集器的改进无关紧要。
PS 以上结果是在使用 i7 处理器的 Ubuntu Linux x64 上得出的。我使用 Temurin 版本也得到了相同的结果。此外,我在 Windows x64 上尝试了 Oracle JRE 22 与 23,结果相似,显示性能差异在 22 和 23 之间。
在简化的JMH基准测试中可以观察到类似的效果(JDK 23 比 JDK 21 快得多):
为了找出原因,我们将使用
-prof perfasm
分析器运行基准测试并分析生成的代码。它包括 16 次展开的循环迭代,但对于我们的目的而言,查看前两个就足够了:JDK 21
JDK 23
代码基本相同,只是 JDK 23 版本包含两条额外
vpxor
指令。为什么额外的指令可以加快执行速度?线索是
vcvtsi2sd
将整数转换为双精度的 AVX 指令。它有两个源操作数:一个是带有整数的通用寄存器,第二个是 SIMD 寄存器,其中 64-127 位是从中复制的。这会产生对源 SIMD 寄存器的冗余依赖,即使后续代码不使用更高位。将寄存器与其自身进行异或是一种将寄存器(包括其高位)清零的简单技巧。这实质上打破了依赖关系:硬件认识到它不再需要关心 中的 64-127 位
vcvtsi2sd
及后续位vdivsd
,因为高位将始终为零。这是一个性能回归问题JDK-8318562 ,已通过此 PR在 JDK 23 中修复。您可以在此 PR 的评论中找到进一步的解释。
有趣的是,禁用 AVX 指令可以
-XX:UseAVX=0
提高 JDK 21 及更早版本的基准性能。