¿Java JIT hace trampa al ejecutar el código JDK?

4 minutos de lectura

avatar de usuario
koen hendrikx

Estaba evaluando un código y no pude hacer que se ejecutara tan rápido como con java.math.BigInteger, incluso cuando se usa exactamente el mismo algoritmo. así que copié java.math.BigInteger source en mi propio paquete e intenté 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 comentar:

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 usa exactamente el mismo código.

Examiné el código de bytes con javap y comparé 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 está usando algunas optimizaciones precalculadas que no puedo usar en mi código? Siempre entendí que no. ¿Qué explica esta diferencia?

  • Interesante. 1. ¿El resultado es consistente (o simplemente aleatorio)? 2. ¿Puedes intentarlo después de calentar JVM? 3. ¿Puede eliminar el factor aleatorio y proporcionar el mismo conjunto de datos como entrada para ambas pruebas?

    – jmj

    28 de agosto de 2017 a las 5:59


  • ¿Intentó ejecutar su punto de referencia con JMH? openjdk.java.net/projects/code-tools/jmh ? No es tan fácil hacer mediciones correctamente manualmente (calentamiento y todo eso).

    – Roman Puchkovsky

    28 de agosto de 2017 a las 6:09

  • Sí, es muy consistente. Si lo dejo funcionar durante 10 minutos, obtengo la misma diferencia. La semilla aleatoria fija garantiza que ambas ejecuciones obtengan el mismo conjunto de datos.

    – Koen Hendrikx

    28 de agosto de 2017 a las 6:11

  • 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.

    -vg

    28 de agosto de 2017 a las 6:14


avatar de usuario
apagones

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

En particular, BigInteger.multiplyToLen es un método intrínseco en HotSpot. hay un especial implementación de ensamblaje codificado a mano en base fuente JVM, pero solo para arquitectura x86-64.

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

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

  • ¿Por qué el código Java idéntico del usuario no tendría también los mismos intrínsecos sustituidos? ¿El intrínseco es de alguna manera solo para un método específico en la JVM?

    – Presidente James K. Polk

    22 de septiembre de 2021 a las 11:46


  • @PresidentJamesK.Polk El código Java no importa. Se utiliza sólo en el intérprete. El objetivo de los intrínsecos es pasar por alto Código Java del método a favor de la implementación integrada de JVM. Hay una lista codificada de nombres de métodos (con nombres de clases) que están sujetos a reemplazo. Si cambia el nombre de una clase o un método, ya no estará intrínseco.

    – apagones

    22 de septiembre de 2021 a las 12:45

  • Vaya, no sé cómo no lo sabía ya. Mirando esa lista de métodos, finalmente entiendo algunas cosas sobre el rendimiento de Java, especialmente para los métodos atómicos. Gracias.

    – Presidente James K. Polk

    23 de septiembre de 2021 a las 14:52

avatar de usuario
eugenio

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 parece ya no ser intrínseco, pero a su vez llama a un método que es intrínseco:

 @HotSpotIntrinsicCandidate
 private static int[] implMultiplyToLen

Entonces, 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.

¿Ha sido útil esta solución?