O Java JIT trapaceia ao executar o código JDK?

405

Eu estava comparando algum código e não consegui executá-lo tão rápido quanto antes java.math.BigInteger, mesmo usando o mesmo algoritmo. Então, copiei o java.math.BigIntegercódigo-fonte no meu próprio pacote e tentei o seguinte:

//import java.math.BigInteger;

public class MultiplyTest {
    public static void main(String[] args) {
        Random r = new Random(1);
        long tm = 0, count = 0,result=0;
        for (int i = 0; i < 400000; i++) {
            int s1 = 400, s2 = 400;
            BigInteger a = new BigInteger(s1 * 8, r), b = new BigInteger(s2 * 8, r);
            long tm1 = System.nanoTime();
            BigInteger c = a.multiply(b);
            if (i > 100000) {
                tm += System.nanoTime() - tm1;
                count++;
            }
            result+=c.bitLength();
        }
        System.out.println((tm / count) + "nsec/mul");
        System.out.println(result); 
    }
}

Quando eu executo isso (jdk 1.8.0_144-b01 no MacOS), ele gera:

12089nsec/mul
2559044166

Quando o executo com a linha de importação não comentada:

4098nsec/mul
2559044166

É quase três vezes mais rápido ao usar a versão JDK do BigInteger em relação à minha versão, mesmo que esteja usando exatamente o mesmo código.

Examinei o bytecode com javap e comparei a saída do compilador ao executar com as opções:

-Xbatch -XX:-TieredCompilation -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions 
-XX:+PrintInlining -XX:CICompilerCount=1

e ambas as versões parecem gerar o mesmo código. Então, o hotspot está usando algumas otimizações pré-computadas que não posso usar no meu código? Eu sempre entendi que eles não. O que explica essa diferença?

Koen Hendrikx
fonte
29
Interessante. 1. O resultado é consistente (ou apenas aleatório)? 2. Você pode tentar após o aquecimento da JVM? 3. Você pode eliminar o fator aleatório e fornecer o mesmo conjunto de dados de entrada para ambos os testes?
precisa saber é o seguinte
7
Você tentou executar seu benchmark com o JMH openjdk.java.net/projects/code-tools/jmh ? Não é tão fácil fazer as medições corretamente manualmente (aquecimento e tudo mais).
Roman Puchkovskiy 28/08
2
Sim, é muito consistente. Se eu deixá-lo funcionar por 10 minutos, continuo com a mesma diferença. A semente aleatória fixa garante que ambas as execuções obtenham o mesmo conjunto de dados.
Koen Hendrikx 28/08
5
Você provavelmente ainda quer JMH, apenas por precaução. E você deve colocar o BigInteger modificado em algum lugar para que as pessoas possam reproduzir seu teste e verificar se você está executando o que acha que está executando.
PVG

Respostas:

529

Sim, o HotSpot JVM é uma espécie de "trapaça", porque possui uma versão especial de alguns BigIntegermétodos que você não encontra no código Java. Esses métodos são chamados de intrínsecos da JVM .

Em particular, BigInteger.multiplyToLené um método instrínseco no HotSpot. Há uma implementação de montagem codificada manualmente na base de origem da JVM, mas apenas para a arquitetura x86-64.

Você pode desativar este instrinsic com a -XX:-UseMultiplyToLenIntrinsicopção de forçar a JVM a usar a implementação Java pura. Nesse caso, o desempenho será semelhante ao desempenho do seu código copiado.

PS Aqui está uma lista de outros métodos intrínsecos do HotSpot.

apangin
fonte
141

No Java 8, esse é realmente um método intrínseco; uma versão ligeiramente modificada do método:

 private static BigInteger test() {

    Random r = new Random(1);
    BigInteger c = null;
    for (int i = 0; i < 400000; i++) {
        int s1 = 400, s2 = 400;
        BigInteger a = new BigInteger(s1 * 8, r), b = new BigInteger(s2 * 8, r);
        c = a.multiply(b);
    }
    return c;
}

Executando isso com:

 java -XX:+UnlockDiagnosticVMOptions  
      -XX:+PrintInlining 
      -XX:+PrintIntrinsics 
      -XX:CICompilerCount=2 
      -XX:+PrintCompilation   
       <YourClassName>

Isso imprimirá muitas linhas e uma delas será:

 java.math.BigInteger::multiplyToLen (216 bytes)   (intrinsic)

No Java 9, por outro lado, esse método parece não ser mais intrínseco, mas, por sua vez, chama um método que é intrínseco:

 @HotSpotIntrinsicCandidate
 private static int[] implMultiplyToLen

Portanto, a execução do mesmo código no Java 9 (com os mesmos parâmetros) revelará:

java.math.BigInteger::implMultiplyToLen (216 bytes)   (intrinsic)

Abaixo, está o mesmo código do método - apenas uma nomeação ligeiramente diferente.

Eugene
fonte