Soy nuevo en Java y estaba ejecutando un código anoche, y esto realmente me molestó. Estaba construyendo un programa simple para mostrar cada X salidas en un bucle for, y noté una disminución ENORME en el rendimiento, cuando usé el módulo como variable % variable vs variable % 5000 o lo que sea. ¿Puede alguien explicarme por qué es esto y qué lo está causando? para poder ser mejor...
Aquí está el código "eficiente" (lo siento si me equivoco un poco en la sintaxis, no estoy en la computadora con el código en este momento)
long startNum = 0; long stopNum = 1000000000L; for (long i = startNum; i <= stopNum; i++){ if (i % 50000 == 0) { System.out.println(i); } }Aquí está el "código ineficiente"
long startNum = 0; long stopNum = 1000000000L; long progressCheck = 50000; for (long i = startNum; i <= stopNum; i++){ if (i % progressCheck == 0) { System.out.println(i); } } Tenga en cuenta que tenía una variable de fecha para medir las diferencias, y una vez que se hizo lo suficientemente larga, la primera tomó 50 ms mientras que la otra tomó 12 segundos o algo así. Es posible que deba aumentar stopNum o disminuir el progressCheck Verifique si su PC es más eficiente que la mía o qué no.
Busqué esta pregunta en la web, pero no puedo encontrar una respuesta, tal vez no la estoy haciendo bien.
EDITAR: No esperaba que mi pregunta fuera tan popular, agradezco todas las respuestas. Realicé un punto de referencia en cada mitad del tiempo empleado, y el código ineficiente tardó considerablemente más, 1/4 de segundo frente a 10 segundos más o menos. De acuerdo, están usando println, pero ambos están haciendo la misma cantidad, por lo que no me imagino que eso lo distorsionaría mucho, especialmente porque la discrepancia es repetible. En cuanto a las respuestas, como soy nuevo en Java, dejaré que los votos decidan por ahora cuál es la mejor respuesta. Intentaré elegir uno para el miércoles.
EDIT2: Voy a hacer otra prueba esta noche, donde en lugar de módulo, solo incrementa una variable, y cuando llega a ProgressCheck, realizará una y luego restablecerá esa variable a 0. para una tercera opción.
EDIT3.5:
Utilicé este código y, a continuación, mostraré mis resultados. ¡Gracias a TODOS por la maravillosa ayuda! También intenté comparar el valor corto del largo con 0, por lo que todas mis nuevas comprobaciones ocurren siempre "65536" veces, lo que hace que sea igual en repeticiones.
public class Main { public static void main(String[] args) { long startNum = 0; long stopNum = 1000000000L; long progressCheck = 65536; final long finalProgressCheck = 50000; long date; // using a fixed value date = System.currentTimeMillis(); for (long i = startNum; i <= stopNum; i++) { if (i % 65536 == 0) { System.out.println(i); } } long final1 = System.currentTimeMillis() - date; date = System.currentTimeMillis(); //using a variable for (long i = startNum; i <= stopNum; i++) { if (i % progressCheck == 0) { System.out.println(i); } } long final2 = System.currentTimeMillis() - date; date = System.currentTimeMillis(); // using a final declared variable for (long i = startNum; i <= stopNum; i++) { if (i % finalProgressCheck == 0) { System.out.println(i); } } long final3 = System.currentTimeMillis() - date; date = System.currentTimeMillis(); // using increments to determine progressCheck int increment = 0; for (long i = startNum; i <= stopNum; i++) { if (increment == 65536) { System.out.println(i); increment = 0; } increment++; } //using a short conversion long final4 = System.currentTimeMillis() - date; date = System.currentTimeMillis(); for (long i = startNum; i <= stopNum; i++) { if ((short)i == 0) { System.out.println(i); } } long final5 = System.currentTimeMillis() - date; System.out.println( "\nfixed = " + final1 + " ms " + "\nvariable = " + final2 + " ms " + "\nfinal variable = " + final3 + " ms " + "\nincrement = " + final4 + " ms" + "\nShort Conversion = " + final5 + " ms"); } }Resultados:
Como era de esperar, debido a la falta de división, la conversión corta fue un 23 % más rápida que la forma "rápida". Esto es interesante de notar. Si necesita mostrar o comparar algo cada 256 veces (o más o menos), puede hacer esto y usar
if ((byte)integer == 0) {'Perform progress check code here'}UNA NOTA INTERESANTE FINAL, el uso del módulo en la "Variable declarada final" con 65536 (no es un número bonito) fue la mitad de la velocidad (más lenta) que el valor fijo. Donde antes estaba comparando cerca de la misma velocidad.
También me sorprende ver el rendimiento de los códigos anteriores. Se trata del tiempo que tarda el compilador en ejecutar el programa según la variable declarada. En el segundo (ineficiente) ejemplo:
for (long i = startNum; i <= stopNum; i++) { if (i % progressCheck == 0) { System.out.println(i) } } Está realizando la operación de módulo entre dos variables. Aquí, el compilador tiene que verificar el valor de stopNum y progressCheck para ir al bloque de memoria específico ubicado para estas variables cada vez después de cada iteración porque es una variable y su valor puede cambiar.
Es por eso que después de cada iteración, el compilador fue a la ubicación de la memoria para verificar el último valor de las variables. Por lo tanto, en el momento de la compilación, el compilador no pudo crear un código de bytes eficiente.
En el primer ejemplo de código, está realizando un operador de módulo entre una variable y un valor numérico constante que no cambiará durante la ejecución y el compilador no necesita verificar el valor de ese valor numérico desde la ubicación de la memoria. Es por eso que el compilador pudo crear un código de bytes eficiente. Si declaras progressCheck como una variable final static final o final, entonces, en el momento del tiempo de ejecución/compilación, el compilador sabe que es una variable final y su valor no va a cambiar, entonces el compilador reemplaza el progressCheck con 50000 en el código:
for (long i = startNum; i <= stopNum; i++) { if (i % 50000== 0) { System.out.println(i) } }Ahora puede ver que este código también se parece al primer ejemplo de código (eficiente). El rendimiento del primer código y, como mencionamos anteriormente, ambos códigos funcionarán de manera eficiente. No habrá mucha diferencia en el tiempo de ejecución de cualquiera de los ejemplos de código.
En seguimiento al comentario de @phuclv , revisé el código generado por JIT 1 , los resultados son los siguientes:
para variable % 5000 (división por constante):
mov rax,29f16b11c6d1e109h imul rbx mov r10,rbx sar r10,3fh sar rdx,0dh sub rdx,r10 imul r10,rdx,0c350h ; <-- imul mov r11,rbx sub r11,r10 test r11,r11 jne 1d707ad14a0h para variable % variable :
mov rax,r14 mov rdx,8000000000000000h cmp rax,rdx jne 22ccce218edh xor edx,edx cmp rbx,0ffffffffffffffffh je 22ccce218f2h cqo idiv rax,rbx ; <-- idiv test rdx,rdx jne 22ccce218c0hDado que la división siempre lleva más tiempo que la multiplicación, el último fragmento de código tiene menos rendimiento.
Versión Java:
java version "11" 2018-09-25 Java(TM) SE Runtime Environment 18.9 (build 11+28) Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11+28, mixed mode) 1 - Opciones de VM utilizadas: -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,src/java/Main.main
Está midiendo el talón OSR (reemplazo en la pila) .
El código auxiliar OSR es una versión especial del método compilado diseñado específicamente para transferir la ejecución del modo interpretado al código compilado mientras se ejecuta el método.
Los resguardos OSR no están tan optimizados como los métodos regulares, porque necesitan un diseño de marco compatible con el marco interpretado. Ya mostré esto en las siguientes respuestas: 1 , 2 , 3 .
Aquí también sucede algo similar. Mientras que el "código ineficiente" ejecuta un bucle largo, el método se compila especialmente para el reemplazo en la pila justo dentro del bucle. El estado se transfiere del marco interpretado al método compilado por OSR, y este estado incluye la variable local progressCheck . En este punto, JIT no puede reemplazar la variable con la constante y, por lo tanto, no puede aplicar ciertas optimizaciones como la reducción de fuerza .
En particular, esto significa que JIT no reemplaza la división de enteros con la multiplicación . (Consulte ¿Por qué GCC usa la multiplicación por un número extraño al implementar la división de enteros? para ver el truco de ASM de un compilador adelantado, cuando el valor es una constante de tiempo de compilación después de insertar/propagación constante, si esas optimizaciones están habilitadas Un literal entero a la derecha en la expresión % también se optimiza mediante gcc -O0 , similar a aquí, donde JITer lo optimiza incluso en un código auxiliar OSR).
Sin embargo, si ejecuta el mismo método varias veces, la segunda ejecución y las subsiguientes ejecutarán el código normal (no OSR), que está completamente optimizado. Aquí hay un punto de referencia para probar la teoría ( comparado usando JMH ):
@State(Scope.Benchmark) public class Div { @Benchmark public void divConst(Blackhole blackhole) { long startNum = 0; long stopNum = 100000000L; for (long i = startNum; i <= stopNum; i++) { if (i % 50000 == 0) { blackhole.consume(i); } } } @Benchmark public void divVar(Blackhole blackhole) { long startNum = 0; long stopNum = 100000000L; long progressCheck = 50000; for (long i = startNum; i <= stopNum; i++) { if (i % progressCheck == 0) { blackhole.consume(i); } } } }Y los resultados:
# Benchmark: bench.Div.divConst # Run progress: 0,00% complete, ETA 00:00:16 # Fork: 1 of 1 # Warmup Iteration 1: 126,967 ms/op # Warmup Iteration 2: 105,660 ms/op # Warmup Iteration 3: 106,205 ms/op Iteration 1: 105,620 ms/op Iteration 2: 105,789 ms/op Iteration 3: 105,915 ms/op Iteration 4: 105,629 ms/op Iteration 5: 105,632 ms/op # Benchmark: bench.Div.divVar # Run progress: 50,00% complete, ETA 00:00:09 # Fork: 1 of 1 # Warmup Iteration 1: 844,708 ms/op <-- much slower! # Warmup Iteration 2: 105,893 ms/op <-- as fast as divConst # Warmup Iteration 3: 105,601 ms/op Iteration 1: 105,570 ms/op Iteration 2: 105,475 ms/op Iteration 3: 105,702 ms/op Iteration 4: 105,535 ms/op Iteration 5: 105,766 ms/op La primera iteración de divVar es, de hecho, mucho más lenta, debido a un stub OSR compilado de manera ineficiente. Pero tan pronto como el método se vuelve a ejecutar desde el principio, se ejecuta la nueva versión sin restricciones que aprovecha todas las optimizaciones del compilador disponibles.
Como han señalado otros, la operación de módulo general requiere que se realice una división. En algunos casos, la división puede ser reemplazada (por el compilador) por una multiplicación. Pero ambos pueden ser lentos en comparación con la suma y la resta. Por lo tanto, se puede esperar el mejor rendimiento con algo como esto:
long progressCheck = 50000; long counter = progressCheck; for (long i = startNum; i <= stopNum; i++){ if (--counter == 0) { System.out.println(i); counter = progressCheck; } } (Como un intento de optimización menor, aquí usamos un contador regresivo de pre-decremento porque en muchas arquitecturas comparar a 0 inmediatamente después de una operación aritmética cuesta exactamente 0 instrucciones/ciclos de CPU porque las banderas de la ALU ya están configuradas apropiadamente por la operación anterior. Una operación decente Sin embargo, el compilador de optimización hará esa optimización automáticamente incluso si escribe if (counter++ == 50000) { ... counter = 0; } .)
Tenga en cuenta que a menudo realmente no quiere/necesita módulo, porque sabe que su contador de bucle ( i ) o lo que sea solo se incrementa en 1, y realmente no le importa el resto real que le dará el módulo, solo ver si el contador de incrementos en uno alcanza algún valor.
Otro 'truco' es usar valores/límites de potencia de dos, por ejemplo, progressCheck = 1024; . El módulo, una potencia de dos, se puede calcular rápidamente a través de bit a bit and , es decir, if ( (i & (1024-1)) == 0 ) {...} . Esto también debería ser bastante rápido, y en algunas arquitecturas puede superar el counter explícito anterior.