Soy nuevo en Java, y estaba ejecutando algo de código anoche, y esto realmente me molestó. Estaba creando un programa simple para mostrar cada X salidas en un bucle for, y noté una disminución MASIVA en el rendimiento, cuando usé el módulo como variable % variable
vs variable % 5000
o no. ¿Puede alguien explicarme por qué es esto y qué lo está causando? Entonces puedo ser mejor ...
Aquí está el código "eficiente" (perdón si tengo un poco de sintaxis incorrecta, 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);
}
}
Eso sí, tenía una variable de fecha para medir las diferencias, y una vez que se hizo lo suficientemente larga, la primera tardó 50 ms, mientras que la otra tardó 12 segundos o algo así. Es posible que tenga que aumentar stopNum
o disminuir progressCheck
si su PC es más eficiente que la mía o no.
Busqué esta pregunta en la web, pero no puedo encontrar una respuesta, tal vez simplemente no la estoy preguntando correctamente.
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 tomó mucho más tiempo, 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 distorsione mucho, especialmente porque la discrepancia es repetible. En cuanto a las respuestas, dado que soy nuevo en Java, dejaré que los votos decidan por ahora qué respuesta es la mejor. 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 alcanza el Progreso de comprobación, realizará una, y luego restablecerá esa variable a 0. para una tercera opción.
EDITAR 3.5:
Usé 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 suceden "65536" veces, haciéndolo 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:
- fijo = 874 ms (normalmente alrededor de 1000 ms, pero más rápido debido a que es una potencia de 2)
- variable = 8590 ms
- variable final = 1944 ms (Fue ~ 1000ms cuando se usa 50000)
- incremento = 1904 ms
- Conversión corta = 679 ms
No es sorprendente, 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 allá), puede hacer esto y usar
if ((byte)integer == 0) {'Perform progress check code here'}
UNA NOTA DE INTERÉS FINAL, el uso del módulo en la "Variable declarada final" con 65536 (no un número bonito) era la mitad de la velocidad (más lenta) que el valor fijo. Donde antes era benchmarking cerca de la misma velocidad.
fuente
final
delante de laprogressCheck
variable, ambos corren a la misma velocidad nuevamente. Eso me lleva a creer que el compilador o el JIT logra optimizar el ciclo cuando sabe queprogressCheck
es constante.Respuestas:
Está midiendo el código auxiliar OSR (reemplazo en la pila) .
El código auxiliar de OSR es una versión especial del método compilado destinado específicamente a transferir la ejecución del modo interpretado al código compilado mientras se ejecuta el método.
Los apéndices OSR no están tan optimizados como los métodos normales, porque necesitan un diseño de marco compatible con el marco interpretado. Ya mostré esto en las siguientes respuestas: 1 , 2 , 3 .
Algo similar sucede aquí también. Mientras que "código ineficiente" ejecuta un ciclo largo, el método se compila especialmente para el reemplazo en la pila dentro del ciclo. El estado se transfiere del marco interpretado al método compilado por OSR, y este estado incluye
progressCheck
la variable local. 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 la 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 en la implementación de la división de enteros? Para el truco asm de un compilador adelantado, cuando el valor es una constante de tiempo de compilación después de la línea / propagación constante, si esas optimizaciones están habilitadas
%
También se optimiza un literal entero en la expresióngcc -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 y las ejecuciones posteriores ejecutarán el código regular (no OSR), que está completamente optimizado. Aquí hay un punto de referencia para probar la teoría ( comparada con JMH ):
Y los resultados:
La primera iteración de
divVar
es de hecho mucho más lenta, debido al código auxiliar 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 de compilador disponibles.fuente
System.out.println
casi necesariamente producirá resultados basura, y el hecho de que ambas versiones sean igualmente rápidas no tiene que ver nada con OSR en particular , por lo que puedo decir ...1
es un poco dudoso: el bucle vacío también podría optimizarse por completo. El segundo es más similar a ese. Pero, de nuevo, no está claro por qué atribuyes la diferencia específicamente a la OSR . Solo diría: en algún momento, el método se JIT y se vuelve más rápido. Según tengo entendido, la OSR solo hace que el uso del código optimizado final sea (aproximadamente) "diferido para el próximo paso de optimización". (continuación ...)%
operación tendría el mismo peso, ya que una ejecución optimizada solo es posible, bueno, si un optimizador hiciera un trabajo real. Por lo tanto, el hecho de que una variante de bucle sea significativamente más rápida que la otra prueba la presencia de un optimizador y demuestra que no pudo optimizar uno de los bucles en el mismo grado que el otro (¡dentro del mismo método!). Como esta respuesta demuestra la capacidad de optimizar ambos bucles en el mismo grado, debe haber algo que dificulte la optimización. Y eso es OSR en el 99.9% de todos los casos-XX:+PrintCompilation -XX:+TraceNMethodInstalls
.En seguimiento al comentario de @phuclv , verifiqué el código generado por JIT 1 , los resultados son los siguientes:
para
variable % 5000
(división por constante):para
variable % variable
:Debido a que la división siempre lleva más tiempo que la multiplicación, el último fragmento de código tiene menos rendimiento.
Versión de Java:
1 - Opciones de VM utilizadas:
-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,src/java/Main.main
fuente
imul
es 3 ciclos,idiv
es entre 30 y 90 ciclos. Entonces la división entera es entre 10x y 30x más lenta que la multiplicación entera.Como otros han señalado, 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 / resta. Por lo tanto, se puede esperar el mejor rendimiento por algo en este sentido:
(Como un pequeño intento de optimización, usamos un contador descendente de pre-decremento aquí porque en muchas arquitecturas en comparación con
0
inmediatamente después de una operación aritmética cuesta exactamente 0 instrucciones / ciclos de CPU porque los indicadores de la ALU ya están configurados adecuadamente por la operación anterior. Una optimización decente sin embargo, el compilador hará esa optimización automáticamente incluso si escribeif (counter++ == 50000) { ... counter = 0; }
).Tenga en cuenta que a menudo no desea / necesita un 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 vea si el contador incremental por uno alcanza algún valor.Otro 'truco' es usar valores / límites de potencia de dos, por ejemplo
progressCheck = 1024;
. Módulo de una potencia de dos se puede calcular rápidamente a través de bit a bitand
, es decirif ( (i & (1024-1)) == 0 ) {...}
. Esto también debería ser bastante rápido, y en algunas arquitecturas puede superar el explícitocounter
anterior.fuente
if()
cuerpo se convierte en un cuerpo de bucle externo, y las cosas externas seif()
convierten en un cuerpo de bucle interno que se ejecuta paramin(progressCheck, stopNum-i)
iteraciones. Entonces, al comienzo, y cada vez quecounter
llega a 0,long next_stop = i + min(progressCheck, stopNum-i);
debe configurar unfor(; i< next_stop; i++) {}
ciclo. En este caso, el bucle interno está vacío y, con suerte, debería optimizarse por completo, puede hacerlo en la fuente y facilitar el JITer, reduciendo su bucle a i + = 50k.--counter
es tan rápido como mi versión incremental, pero menos código. Además, fue 1 más bajo de lo que debería ser, tengo curiosidad por saber si debería sercounter--
para obtener el número exacto que desea , no es que sea una gran diferencia--counter
está apagado por uno.counter--
le dará exactamente elprogressCheck
número de iteraciones (o podría establecer,progressCheck = 50001;
por supuesto).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 ejemplo (ineficiente):
Está realizando la operación de módulo entre dos variables. Aquí, el compilador tiene que verificar el valor de
stopNum
yprogressCheck
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 compilador de iteración 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 va a cambiar dentro de 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 se declara
progressCheck
como unafinal
o como unafinal static
variable de entonces en el momento de tiempo de ejecución / know compilador de tiempo de compilación que se trata de una variable final y su valor no va a cambiar a continuación, reemplazar el compiladorprogressCheck
con50000
en código: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 ninguno de los ejemplos de código.
fuente
volatile
el 'compilador' no leerá su valor de la RAM una y otra vez.