Czy Java JIT oszukiwa podczas uruchamiania kodu JDK?

405

Testowałem trochę kodu i nie mogłem go uruchomić tak szybko, jak z java.math.BigInteger, nawet przy użyciu tego samego algorytmu. Skopiowałem więc java.math.BigIntegerźródło do własnego pakietu i wypróbowałem:

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

Kiedy uruchomię to (jdk 1.8.0_144-b01 na MacOS), wyświetla:

12089nsec/mul
2559044166

Kiedy uruchamiam go z niepomocowaną linią importu:

4098nsec/mul
2559044166

Jest prawie trzy razy szybszy, gdy używasz wersji BigInteger JDK w porównaniu z moją wersją, nawet jeśli używa dokładnie tego samego kodu.

Sprawdziłem kod bajtowy za pomocą javap i porównałem dane wyjściowe kompilatora podczas pracy z opcjami:

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

i obie wersje wydają się generować ten sam kod. Czy punkt aktywny korzysta z niektórych wstępnie obliczonych optymalizacji, których nie mogę użyć w kodzie? Zawsze rozumiałem, że nie. Co wyjaśnia tę różnicę?

Koen Hendrikx
źródło
29
Ciekawy. 1. Czy wynik jest spójny (czy po prostu szczęśliwy losowo)? 2. Czy możesz spróbować po rozgrzaniu JVM? 3. Czy możesz wyeliminować czynnik losowy i podać ten sam zestaw danych jako dane wejściowe dla obu testów?
Jigar Joshi
7
Czy próbowałeś uruchomić swój test porównawczy z JMH openjdk.java.net/projects/code-tools/jmh ? Właściwe ręczne dokonywanie pomiarów nie jest takie proste (rozgrzewka i tak dalej).
Roman Puchkovskiy
2
Tak, jest to bardzo konsekwentne. Jeśli pozwolę mu działać przez 10 minut, nadal będę mieć tę samą różnicę. Naprawione losowe ziarno zapewnia, że ​​oba przebiegi otrzymają ten sam zestaw danych.
Koen Hendrikx
5
Prawdopodobnie nadal chcesz JMH, na wszelki wypadek. I powinieneś gdzieś umieścić zmodyfikowany BigInteger, aby ludzie mogli odtworzyć test i sprawdzić, czy korzystasz z tego, co według Ciebie działa.
pvg

Odpowiedzi:

529

Tak, HotSpot JVM jest swego rodzaju „oszustwem”, ponieważ ma specjalną wersję niektórych BigIntegermetod, których nie znajdziesz w kodzie Java. Metody te nazywane są wewnętrznymi elementami JVM .

W szczególności BigInteger.multiplyToLenjest to metoda instruktażowa w HotSpot. Istnieje specjalna ręcznie kodowana implementacja zestawu w bazie źródłowej JVM, ale tylko dla architektury x86-64.

Możesz wyłączyć tę instruktaż z -XX:-UseMultiplyToLenIntrinsicopcją zmuszenia JVM do korzystania z czystej implementacji Java. W takim przypadku wydajność będzie podobna do wydajności skopiowanego kodu.

PS Oto lista innych wewnętrznych metod HotSpot.

apangin
źródło
141

W Javie 8 jest to rzeczywiście metoda wewnętrzna; nieco zmodyfikowana wersja metody:

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

Uruchamianie tego z:

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

Spowoduje to wydrukowanie wielu linii, a jedną z nich będzie:

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

Z drugiej strony w Javie 9 metoda ta wydaje się już nie być wewnętrzną, ale z kolei wywołuje metodę wewnętrzną:

 @HotSpotIntrinsicCandidate
 private static int[] implMultiplyToLen

Zatem uruchomienie tego samego kodu w Javie 9 (z tymi samymi parametrami) ujawni:

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

Poniżej znajduje się ten sam kod dla metody - tylko nieco inne nazewnictwo.

Eugene
źródło