tutorial libreria example español descargar java for-loop jvm

libreria - ¿Por qué un bucle Java de 4 mil millones de iteraciones toma solo 2 ms?



libreria swing java netbeans (6)

Aquí hay una de dos posibilidades:

  1. El compilador se dio cuenta de que el bucle es redundante y no hace nada, por lo que lo optimizó.

  2. El JIT (compilador justo a tiempo) se dio cuenta de que el bucle es redundante y no hace nada, por lo que lo optimizó.

Los compiladores modernos son muy inteligentes; pueden ver cuando el código es inútil. Intente colocar un bucle vacío en GodBolt y mire la salida, luego active las optimizaciones de -O2 , verá que la salida es algo -O2

main(): xor eax, eax ret

Me gustaría aclarar algo, en Java la mayoría de las optimizaciones las realiza el JIT. En algunos otros lenguajes (como C / C ++), la mayoría de las optimizaciones las realiza el primer compilador.

Estoy ejecutando el siguiente código Java en una computadora portátil con Intel Core i7 de 2.7 GHz. Tenía la intención de dejar que midiera cuánto tiempo lleva terminar un ciclo con 2 ^ 32 iteraciones, que esperaba que fuera aproximadamente 1.48 segundos (4 / 2.7 = 1.48).

Pero en realidad solo toma 2 milisegundos, en lugar de 1.48 s. Me pregunto si esto es el resultado de alguna optimización JVM debajo.

public static void main(String[] args) { long start = System.nanoTime(); for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++){ } long finish = System.nanoTime(); long d = (finish - start) / 1000000; System.out.println("Used " + d); }


Como ya se señaló, el compilador JIT (justo a tiempo) puede optimizar un bucle vacío para eliminar iteraciones innecesarias. ¿Pero cómo?

En realidad, hay dos compiladores JIT: C1 y C2 . Primero, el código se compila con el C1. C1 recopila las estadísticas y ayuda a la JVM a descubrir que en un 100% de los casos nuestro bucle vacío no cambia nada y es inútil. En esta situación, C2 entra en escena. Cuando se llama al código con mucha frecuencia, se puede optimizar y compilar con el C2 utilizando estadísticas recopiladas.

Como ejemplo, probaré el siguiente fragmento de código (mi JDK está configurado para slowdebug build 9-internal ):

public class Demo { private static void run() { for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) { } System.out.println("Done!"); } }

Con las siguientes opciones de línea de comando:

-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*Demo.run

Y hay diferentes versiones de mi método de ejecución , compiladas con el C1 y C2 adecuadamente. Para mí, la variante final (C2) se parece a esto:

... ; B1: # B3 B2 <- BLOCK HEAD IS JUNK Freq: 1 0x00000000125461b0: mov dword ptr [rsp+0ffffffffffff7000h], eax 0x00000000125461b7: push rbp 0x00000000125461b8: sub rsp, 40h 0x00000000125461bc: mov ebp, dword ptr [rdx] 0x00000000125461be: mov rcx, rdx 0x00000000125461c1: mov r10, 57fbc220h 0x00000000125461cb: call indirect r10 ; *iload_1 0x00000000125461ce: cmp ebp, 7fffffffh ; 7fffffff => 2147483647 0x00000000125461d4: jnl 125461dbh ; jump if not less ; B2: # B3 <- B1 Freq: 0.999999 0x00000000125461d6: mov ebp, 7fffffffh ; *if_icmpge ; B3: # N44 <- B1 B2 Freq: 1 0x00000000125461db: mov edx, 0ffffff5dh 0x0000000012837d60: nop 0x0000000012837d61: nop 0x0000000012837d62: nop 0x0000000012837d63: call 0ae86fa0h ...

Es un poco desordenado, pero si observa de cerca, puede notar que no hay un ciclo de larga duración aquí. Hay 3 bloques: B1, B2 y B3 y los pasos de ejecución pueden ser B1 -> B2 -> B3 o B1 -> B3 . Donde Freq: 1 - frecuencia estimada normalizada de ejecución de un bloque.


Considera el tiempo de inicio y finalización en nanosegundos y divide entre 10 ^ 6 para calcular la latencia

long d = (finish - start) / 1000000

debería ser 10^9 porque 1 segundo = 10^9 nanosegundos.


Estás midiendo el tiempo que lleva detectar el bucle no hace nada, compila el código en un hilo de fondo y elimina el código.

for (int t = 0; t < 5; t++) { long start = System.nanoTime(); for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) { } long time = System.nanoTime() - start; String s = String.format("%d: Took %.6f ms", t, time / 1e6); Thread.sleep(50); System.out.println(s); Thread.sleep(50); }

Si ejecuta esto con -XX:+PrintCompilation , puede ver que el código se ha compilado en segundo plano para el compilador de nivel 3 o C1 y después de algunos bucles al nivel 4 de C4.

129 34 % 3 A::main @ 15 (93 bytes) 130 35 3 A::main (93 bytes) 130 36 % 4 A::main @ 15 (93 bytes) 131 34 % 3 A::main @ -2 (93 bytes) made not entrant 131 36 % 4 A::main @ -2 (93 bytes) made not entrant 0: Took 2.510408 ms 268 75 % 3 A::main @ 15 (93 bytes) 271 76 % 4 A::main @ 15 (93 bytes) 274 75 % 3 A::main @ -2 (93 bytes) made not entrant 1: Took 5.629456 ms 2: Took 0.000000 ms 3: Took 0.000364 ms 4: Took 0.000365 ms

Si cambia el ciclo para usar un long , no se optimiza tanto.

for (long i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) { }

en cambio obtienes

0: Took 1579.267321 ms 1: Took 1674.148662 ms 2: Took 1885.692166 ms 3: Took 1709.870567 ms 4: Took 1754.005112 ms


Parece que fue optimizado por el compilador JIT. Cuando lo -Djava.compiler=NONE ( -Djava.compiler=NONE ), el código se ejecuta mucho más lento:

$ javac MyClass.java $ java MyClass Used 4 $ java -Djava.compiler=NONE MyClass Used 40409

Puse el código de OP dentro de la class MyClass .


Solo diré lo obvio: que esta es una optimización JVM que sucede, el bucle simplemente se eliminará en absoluto. Aquí hay una pequeña prueba que muestra la gran diferencia que tiene JIT cuando está habilitado / habilitado solo para C1 Compiler y está deshabilitado en absoluto.

Descargo de responsabilidad: no escriba pruebas como esta, esto es solo para demostrar que la "eliminación" del bucle real ocurre en el C2 Compiler :

@Benchmark @Fork(1) public void full() { long result = 0; for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) { ++result; } } @Benchmark @Fork(1) public void minusOne() { long result = 0; for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) { ++result; } } @Benchmark @Fork(value = 1, jvmArgsAppend = { "-XX:TieredStopAtLevel=1" }) public void withoutC2() { long result = 0; for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) { ++result; } } @Benchmark @Fork(value = 1, jvmArgsAppend = { "-Xint" }) public void withoutAll() { long result = 0; for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) { ++result; } }

Los resultados muestran que, dependiendo de qué parte del JIT esté habilitado, el método se vuelve más rápido (tanto más rápido que parece que no está haciendo "nada" - eliminación de bucles, que parece estar sucediendo en el C2 Compiler - que es el nivel máximo) :

Benchmark Mode Cnt Score Error Units Loop.full avgt 2 ≈ 10⁻⁷ ms/op Loop.minusOne avgt 2 ≈ 10⁻⁶ ms/op Loop.withoutAll avgt 2 51782.751 ms/op Loop.withoutC2 avgt 2 1699.137 ms/op