Betrügt Java JIT beim Ausführen von JDK-Code?

405

Ich habe Code verglichen und konnte ihn nicht so schnell zum Laufen bringen wie mit java.math.BigInteger, selbst wenn genau derselbe Algorithmus verwendet wurde. Also habe ich die java.math.BigIntegerQuelle in mein eigenes Paket kopiert und Folgendes versucht:

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

Wenn ich dies ausführe (jdk 1.8.0_144-b01 unter MacOS), wird Folgendes ausgegeben:

12089nsec/mul
2559044166

Wenn ich es mit der unkommentierten Importzeile ausführe:

4098nsec/mul
2559044166

Es ist fast dreimal so schnell, wenn Sie die JDK-Version von BigInteger verwenden, im Vergleich zu meiner Version, selbst wenn genau derselbe Code verwendet wird.

Ich habe den Bytecode mit Javap untersucht und die Compilerausgabe beim Ausführen mit Optionen verglichen:

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

und beide Versionen scheinen den gleichen Code zu generieren. Verwendet der Hotspot also einige vorberechnete Optimierungen, die ich in meinem Code nicht verwenden kann? Ich habe immer verstanden, dass sie es nicht tun. Was erklärt diesen Unterschied?

Koen Hendrikx
quelle
29
Interessant. 1. Ist das Ergebnis konsistent (oder nur zufällig)? 2. Können Sie es nach dem Aufwärmen von JVM versuchen? 3. Können Sie Zufallsfaktoren eliminieren und denselben Datensatz als Eingabe für beide Tests bereitstellen?
Jigar Joshi
7
Haben Sie versucht, Ihren Benchmark mit JMH openjdk.java.net/projects/code-tools/jmh auszuführen ? Es ist nicht so einfach, Messungen manuell korrekt durchzuführen (Aufwärmen und all das Zeug).
Roman Puchkovskiy
2
Ja, es ist sehr konsequent. Wenn ich es 10 Minuten laufen lasse, bekomme ich immer noch den gleichen Unterschied. Der feste zufällige Startwert stellt sicher, dass beide Läufe denselben Datensatz erhalten.
Koen Hendrikx
5
Sie wollen wahrscheinlich immer noch JMH, nur für den Fall. Und Sie sollten Ihre modifizierte BigInteger irgendwo aufstellen, damit die Leute Ihren Test reproduzieren und überprüfen können, ob Sie das ausführen, was Sie zu tun glauben.
pvg

Antworten:

529

Ja, HotSpot JVM ist eine Art "Betrug", da es eine spezielle Version einiger BigIntegerMethoden gibt, die Sie im Java-Code nicht finden. Diese Methoden werden als JVM-Intrinsics bezeichnet .

Insbesondere BigInteger.multiplyToLenist eine instrumentelle Methode in HotSpot. Es gibt eine spezielle handcodierte Assembly-Implementierung in der JVM-Quellbasis, jedoch nur für die x86-64-Architektur.

Sie können dieses Instrument mit der -XX:-UseMultiplyToLenIntrinsicOption deaktivieren , JVM zur Verwendung einer reinen Java-Implementierung zu zwingen. In diesem Fall entspricht die Leistung der Leistung Ihres kopierten Codes.

PS Hier ist eine Liste anderer HotSpot-Methoden.

Apangin
quelle
141

In Java 8 ist dies in der Tat eine intrinsische Methode. eine leicht modifizierte Version der Methode:

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

Führen Sie dies aus mit:

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

Dadurch werden viele Zeilen gedruckt, und eine davon ist:

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

In Java 9 hingegen scheint diese Methode nicht mehr intrinsisch zu sein, sondern ruft eine Methode auf, die intrinsisch ist:

 @HotSpotIntrinsicCandidate
 private static int[] implMultiplyToLen

Wenn Sie also denselben Code unter Java 9 (mit denselben Parametern) ausführen, wird Folgendes angezeigt:

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

Darunter befindet sich der gleiche Code für die Methode - nur eine etwas andere Benennung.

Eugene
quelle