Estou tentando o seguinte código para verificação de desempenho. Ele é puramente limitado pela CPU, faz muitos cálculos em double
tipos, single-threaded e não usa nenhum heap:
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;
}
}
Agora, usando o mesmo arquivo de classe compilado, compare os resultados ao executar no JRE 21 versus 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
Em cada execução, há uma fase de aquecimento nas duas primeiras iterações, mas a partir da iteração 3 é o mais rápido possível.
O que mudou no Java 23 para tornar isso mais rápido? Ao olhar as notas de lançamento, tudo o que consigo encontrar sobre desempenho são melhorias no coletor de lixo. Mas não estamos usando o heap aqui, então a melhoria do coletor de lixo é irrelevante.
PS Os resultados acima são no Ubuntu Linux x64 usando um processador i7. Obtenho os mesmos resultados usando versões Temurin. Além disso, tentei Oracle JRE 22 vs 23 no Windows x64 com resultados semelhantes, mostrando que a diferença de desempenho está entre 22 e 23.
Um efeito semelhante (JDK 23 sendo muito mais rápido que JDK 21) pode ser observado em um benchmark JMH simplificado :
Para descobrir o motivo, executaremos o benchmark com
-prof perfasm
profiler e analisaremos o código gerado. Ele inclui 16 iterações de loop desenroladas, mas para nosso propósito, é suficiente olhar para as duas primeiras:JDK 21
JDK 23
O código é praticamente o mesmo, exceto que a versão JDK 23 contém duas
vpxor
instruções extras. Como instruções extras resultam em execução mais rápida?A dica é a instrução AVX
vcvtsi2sd
que converte um inteiro para double. Ela tem dois operandos de origem: um é um registrador de propósito geral com um inteiro, e o segundo é um registrador SIMD, de onde os bits 64-127 são copiados. Isso cria dependência redundante no registrador SIMD de origem, mesmo que o código subsequente não use bits mais altos.xor'ing um registrador consigo mesmo é um truque barato para zerar um registrador, incluindo seus bits mais altos. Isso essencialmente quebra a dependência: o hardware reconhece que não precisa mais se importar com os bits 64-127 em
vcvtsi2sd
e subsequentesvdivsd
, pois os bits mais altos serão sempre zero.Esta era uma regressão de desempenho JDK-8318562 que foi corrigida no JDK 23 por este PR . Você pode encontrar mais explicações nos comentários deste PR.
Curiosamente, desabilitar instruções AVX
-XX:UseAVX=0
melhora o desempenho de benchmark no JDK 21 e versões anteriores.