Java JIT обманывает при запуске кода JDK?

405

Я тестировал некоторый код и не мог заставить его работать так же быстро, как java.math.BigIntegerпри использовании того же алгоритма. Поэтому я скопировал java.math.BigIntegerисходный код в свой собственный пакет и попробовал это:

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

Когда я запускаю это (jdk 1.8.0_144-b01 на MacOS), он выдает:

12089nsec/mul
2559044166

Когда я запускаю его со строкой импорта без комментариев:

4098nsec/mul
2559044166

Это почти в три раза быстрее при использовании JDK-версии BigInteger по сравнению с моей версией, даже если она использует точно такой же код.

Я исследовал байт-код с помощью javap и сравнил вывод компилятора при работе с параметрами:

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

и обе версии, кажется, генерируют один и тот же код. Так использует ли горячая точка некоторые предварительно вычисленные оптимизации, которые я не могу использовать в своем коде? Я всегда понимал, что это не так. Чем объясняется эта разница?

Коэн Хендрикс
источник
29
Интересно. 1. Является ли результат последовательным (или просто случайным)? 2. Можете ли вы попробовать после прогрева JVM? 3. Можете ли вы исключить случайный фактор и предоставить один и тот же набор данных в качестве входных данных для обоих тестов?
Джигар Джоши
7
Вы пытались запустить свой тест с JMH openjdk.java.net/projects/code-tools/jmh ? Нелегко правильно выполнить измерения вручную (разогреть и все такое).
Роман Пучковский
2
Да, это очень соответствует. Если я позволю этому работать в течение 10 минут, я все еще получу ту же самую разницу. Фиксированное случайное начальное число гарантирует, что оба прогона получат один и тот же набор данных.
Коэн Хендрикс
5
Вы, вероятно, все еще хотите JMH, на всякий случай. И вы должны где-то поместить ваш модифицированный BigInteger, чтобы люди могли воспроизвести ваш тест и убедиться, что вы выполняете то, что, по вашему мнению, вы запускаете.
Пвг

Ответы:

529

Да, HotSpot JVM является своего рода «обманом», поскольку в нем есть специальная версия некоторых BigIntegerметодов, которые вы не найдете в коде Java. Эти методы называются внутренностями JVM .

В частности, BigInteger.multiplyToLenэто собственный метод в HotSpot. В исходной базе JVM есть специальная реализация сборки с ручным кодированием , но только для архитектуры x86-64.

Вы можете отключить этот экземпляр с помощью -XX:-UseMultiplyToLenIntrinsicопции, чтобы заставить JVM использовать чистую реализацию Java. В этом случае производительность будет аналогична производительности вашего скопированного кода.

PS Вот список других встроенных методов HotSpot.

apangin
источник
141

В Java 8 это действительно встроенный метод; немного измененная версия метода:

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

Запуск этого с:

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

Это напечатает много строк, и одна из них будет:

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

С другой стороны, в Java 9 этот метод больше не является внутренним, но, в свою очередь, он вызывает встроенный метод:

 @HotSpotIntrinsicCandidate
 private static int[] implMultiplyToLen

Таким образом, выполнение того же кода под Java 9 (с теми же параметрами) покажет:

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

Под ним тот же код для метода - просто немного другое наименование.

Евгений
источник