¿Java JIT engaña cuando ejecuta código JDK?

405

Estaba comparando un código y no pude ejecutarlo tan rápido como con java.math.BigIntegerel mismo algoritmo. Así que copié la java.math.BigIntegerfuente en mi propio paquete y probé esto:

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

Cuando ejecuto esto (jdk 1.8.0_144-b01 en MacOS) sale:

12089nsec/mul
2559044166

Cuando lo ejecuto con la línea de importación sin comentarios:

4098nsec/mul
2559044166

Es casi tres veces más rápido cuando se usa la versión JDK de BigInteger en comparación con mi versión, incluso si se usa exactamente el mismo código.

He examinado el bytecode con javap y comparado la salida del compilador cuando se ejecuta con opciones:

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

y ambas versiones parecen generar el mismo código. Entonces, ¿el punto de acceso utiliza algunas optimizaciones precalculadas que no puedo usar en mi código? Siempre entendí que no. ¿Qué explica esta diferencia?

Koen Hendrikx
fuente
29
Interesante. 1. ¿El resultado es consistente (o simplemente al azar)? 2. ¿Puedes probar después de calentar JVM? 3. ¿Puede eliminar el factor aleatorio y proporcionar el mismo conjunto de datos como entrada para la prueba?
Jigar Joshi
77
¿ Intentó ejecutar su punto de referencia con JMH openjdk.java.net/projects/code-tools/jmh ? No es tan fácil hacer mediciones correctamente de forma manual (calentamiento y todo eso).
Roman Puchkovskiy
2
Sí, es muy consistente. Si lo dejo correr durante 10 minutos, sigo teniendo la misma diferencia. La semilla aleatoria fija asegura que ambas ejecuciones obtengan el mismo conjunto de datos.
Koen Hendrikx
55
Probablemente todavía quieras a JMH, por si acaso. Y debe colocar su BigInteger modificado en algún lugar para que las personas puedan reproducir su prueba y verificar que está ejecutando lo que cree que está ejecutando.
pvg

Respuestas:

529

Sí, HotSpot JVM es una especie de "trampa", porque tiene una versión especial de algunos BigIntegermétodos que no encontrará en el código Java. Estos métodos se llaman intrínsecos JVM .

En particular, BigInteger.multiplyToLenes un método instrínseco en HotSpot. Existe una implementación especial de ensamblaje codificado a mano en la base de origen JVM, pero solo para la arquitectura x86-64.

Puede deshabilitar este instrínseco con la -XX:-UseMultiplyToLenIntrinsicopción de forzar a JVM a usar implementación pura de Java. En este caso, el rendimiento será similar al rendimiento de su código copiado.

PD Aquí hay una lista de otros métodos intrínsecos de HotSpot.

apangin
fuente
141

En Java 8, este es de hecho un método intrínseco; Una versión ligeramente modificada del 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;
}

Ejecutando esto con:

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

Esto imprimirá muchas líneas y una de ellas será:

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

En Java 9, por otro lado, ese método ya no parece ser intrínseco, pero a su vez llama a un método que es intrínseco:

 @HotSpotIntrinsicCandidate
 private static int[] implMultiplyToLen

Por lo tanto, ejecutar el mismo código en Java 9 (con los mismos parámetros) revelará:

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

Debajo está el mismo código para el método, solo un nombre ligeramente diferente.

Eugene
fuente