Java JIT triche-t-il lors de l'exécution du code JDK?

405

J'étalonnais du code et je ne pouvais pas le faire fonctionner aussi vite qu'avec java.math.BigInteger, même en utilisant exactement le même algorithme. J'ai donc copié la java.math.BigIntegersource dans mon propre package et essayé ceci:

//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); 
    }
}

Lorsque je lance ceci (jdk 1.8.0_144-b01 sur MacOS), il génère:

12089nsec/mul
2559044166

Lorsque je l'exécute avec la ligne d'importation sans commentaire:

4098nsec/mul
2559044166

C'est presque trois fois plus rapide lorsque vous utilisez la version JDK de BigInteger par rapport à ma version, même si elle utilise exactement le même code.

J'ai examiné le bytecode avec javap et comparé la sortie du compilateur lors de l'exécution avec des options:

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

et les deux versions semblent générer le même code. Le hotspot utilise-t-il des optimisations précalculées que je ne peux pas utiliser dans mon code? J'ai toujours compris que non. Qu'est-ce qui explique cette différence?

Koen Hendrikx
la source
29
Intéressant. 1. Le résultat est-il cohérent (ou simplement aléatoire)? 2. Pouvez-vous essayer après avoir réchauffé la JVM? 3. Pouvez-vous éliminer le facteur aléatoire et fournir le même ensemble de données en entrée pour le test?
Jigar Joshi
7
Avez-vous essayé d'exécuter votre benchmark avec JMH openjdk.java.net/projects/code-tools/jmh ? Ce n'est pas si facile de faire des mesures correctement manuellement (échauffement et tout ça).
Roman Puchkovskiy
2
Oui, c'est très cohérent. Si je le laisse fonctionner pendant 10 minutes, j'obtiens toujours la même différence. La valeur de départ aléatoire fixe garantit que les deux exécutions obtiennent le même ensemble de données.
Koen Hendrikx
5
Vous voulez probablement encore JMH, juste au cas où. Et vous devriez installer votre BigInteger modifié quelque part afin que les gens puissent reproduire votre test et vérifier que vous exécutez ce que vous pensez exécuter.
pvg

Réponses:

529

Oui, HotSpot JVM est une sorte de "tricherie", car il a une version spéciale de certains BigInteger méthodes que vous ne trouverez pas dans le code Java. Ces méthodes sont appelées intrinsèques JVM .

En particulier, BigInteger.multiplyToLenest une méthode intrinsèque dans HotSpot. Il y a un spécial implémentation d'assemblage codée à la main dans la base de la source JVM, mais uniquement pour l'architecture x86-64.

Vous pouvez désactiver cette instruction avec -XX:-UseMultiplyToLenIntrinsic option pour forcer JVM à utiliser une implémentation Java pure. Dans ce cas, les performances seront similaires à celles de votre code copié.

PS Voici une liste d'autres méthodes intrinsèques HotSpot.

apangin
la source
141

En Java 8, il s'agit en effet d'une méthode intrinsèque; une version légèrement modifiée de la méthode:

 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;
}

Exécuter ceci avec:

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

Cela imprimera beaucoup de lignes et l'une d'elles sera:

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

En Java 9, en revanche, cette méthode ne semble plus être intrinsèque, mais à son tour, elle appelle une méthode intrinsèque:

 @HotSpotIntrinsicCandidate
 private static int[] implMultiplyToLen

Donc, exécuter le même code sous Java 9 (avec les mêmes paramètres) révélera:

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

En dessous, c'est le même code pour la méthode - juste un nom légèrement différent.

Eugène
la source