Estoy ejecutando Windows 8.1 x64 con la actualización de Java 7 45 x64 (sin Java de 32 bits instalado) en una tableta Surface Pro 2.
El siguiente código toma 1688ms cuando el tipo de i es largo y 109ms cuando i es un int. ¿Por qué long (un tipo de 64 bits) es un orden de magnitud más lento que int en una plataforma de 64 bits con una JVM de 64 bits?
Mi única especulación es que la CPU tarda más en agregar un entero de 64 bits que uno de 32 bits, pero eso parece poco probable. Sospecho que Haswell no usa sumadores de transporte de ondas.
Estoy ejecutando esto en Eclipse Kepler SR1, por cierto.
public class Main {
private static long i = Integer.MAX_VALUE;
public static void main(String[] args) {
System.out.println("Starting the loop");
long startTime = System.currentTimeMillis();
while(!decrementAndCheck()){
}
long endTime = System.currentTimeMillis();
System.out.println("Finished the loop in " + (endTime - startTime) + "ms");
}
private static boolean decrementAndCheck() {
return --i < 0;
}
}
Editar: Aquí están los resultados del código C ++ equivalente compilado por VS 2013 (abajo), mismo sistema. largo: 72265ms int: 74656ms Esos resultados se obtuvieron en el modo de depuración de 32 bits.
En modo de liberación de 64 bits: largo: 875ms largo largo: 906ms int: 1047ms
Esto sugiere que el resultado que observé es una rareza de optimización de JVM en lugar de limitaciones de CPU.
#include "stdafx.h"
#include "iostream"
#include "windows.h"
#include "limits.h"
long long i = INT_MAX;
using namespace std;
boolean decrementAndCheck() {
return --i < 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
cout << "Starting the loop" << endl;
unsigned long startTime = GetTickCount64();
while (!decrementAndCheck()){
}
unsigned long endTime = GetTickCount64();
cout << "Finished the loop in " << (endTime - startTime) << "ms" << endl;
}
Editar: Intenté esto nuevamente en Java 8 RTM, sin cambios significativos.
fuente
currentTimeMillis()
, ejecutar código que trivialmente se puede optimizar completamente, etc. huele a resultados poco confiables.long
como contador de bucle, porque el compilador JIT optimizó el bucle, cuando usé unint
. Habría que mirar el desmontaje del código de máquina generado.Respuestas:
Mi JVM hace esto bastante sencillo en el bucle interno cuando usa
long
s:Hace trampa, duro, cuando usas
int
s; primero hay algo de locura que no pretendo entender, pero parece una configuración para un bucle desenrollado:luego el bucle desenrollado en sí:
luego, el código de desmontaje para el bucle desenrollado, en sí mismo una prueba y un bucle directo:
Así que va 16 veces más rápido para los ints porque el JIT desenrolló el
int
bucle 16 veces, pero no desenrolló ellong
bucle en absoluto.Para completar, aquí está el código que realmente probé:
Los volcados de ensamblaje se generaron utilizando las opciones
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
. Tenga en cuenta que necesita perder el tiempo con la instalación de su JVM para que esto funcione también para usted; necesita poner alguna biblioteca compartida aleatoria en el lugar correcto o fallará.fuente
long
versión sea más lenta, sino que laint
versión sea más rápida. Eso tiene sentido. Probablemente no se invirtió tanto esfuerzo en hacer que el JIT optimizara laslong
expresiones.gcc
usa-f
como el interruptor de línea de comando para "bandera", y launroll-loops
optimización se activa diciendo-funroll-loops
. Solo uso "desenrollar" para describir la optimización.i-=16
, que por supuesto es 16 veces más rápido.La pila de JVM se define en términos de palabras , cuyo tamaño es un detalle de implementación pero debe tener al menos 32 bits de ancho. El implementador de JVM puede usar palabras de 64 bits, pero el código de bytes no puede depender de esto, por lo que las operaciones con valores
long
odouble
deben manejarse con especial cuidado. En particular, las instrucciones de bifurcación de enteros de JVM se definen exactamente en el tipoint
.En el caso de su código, el desmontaje es instructivo. Aquí está el código de bytes para la
int
versión compilada por Oracle JDK 7:Tenga en cuenta que la JVM cargará el valor de su estática
i
(0), restará uno (3-4), duplicará el valor en la pila (5) y lo devolverá a la variable (6). Luego hace una bifurcación de comparación con cero y regresa.La versión con el
long
es un poco más complicada:Primero, cuando la JVM duplica el nuevo valor en la pila (5), tiene que duplicar dos palabras de pila. En su caso, es muy posible que esto no sea más caro que duplicar uno, ya que la JVM es libre de usar una palabra de 64 bits si es conveniente. Sin embargo, notará que la lógica de la rama es más larga aquí. La JVM no tiene una instrucción para comparar a
long
con cero, por lo que tiene que insertar una constante0L
en la pila (9), hacer unalong
comparación general (10) y luego bifurcar en el valor de ese cálculo.Aquí hay dos escenarios plausibles:
long
versión, presionando y haciendo estallar varios valores adicionales, y estos están en la pila administrada virtual , no en la pila de CPU asistida por hardware real. Si este es el caso, aún verá una diferencia de rendimiento significativa después del calentamiento.Le recomiendo que escriba un microbenchmark correcto para eliminar el efecto de activar el JIT, y también intentar esto con una condición final que no sea cero, para obligar a la JVM a hacer la misma comparación
int
que hace con ellong
.fuente
== 0
, lo que parece ser una parte desproporcionadamente grande de los resultados de referencia. Me parece más probable que OP esté tratando de medir un rango más general de operaciones, y esta respuesta señala que el punto de referencia está muy sesgado hacia solo una de esas operaciones.La unidad básica de datos en una máquina virtual Java es la palabra. La elección del tamaño de palabra correcto se deja en la implementación de la JVM. Una implementación de JVM debe elegir un tamaño de palabra mínimo de 32 bits. Puede elegir un tamaño de palabra más alto para ganar eficiencia. Tampoco existe ninguna restricción de que una JVM de 64 bits deba elegir solo palabras de 64 bits.
La arquitectura subyacente no establece que el tamaño de la palabra también deba ser el mismo. JVM lee / escribe datos palabra por palabra. Esta es la razón por la que podría estar tomando más tiempo para una larga que un int .
Aquí puede encontrar más sobre el mismo tema.
fuente
Acabo de escribir un punto de referencia usando caliper .
Los resultados son bastante consistentes con el código original: una aceleración de ~ 12x para usar
int
overlong
. Ciertamente parece que está ocurriendo el desenrollamiento del bucle informado por tmyklebu o algo muy similar.Este es mi código; tenga en cuenta que utiliza una instantánea recién construida de
caliper
, ya que no pude averiguar cómo codificar contra su versión beta existente.fuente
Para el registro, esta versión hace un crudo "calentamiento":
Los tiempos generales mejoran alrededor del 30%, pero la relación entre los dos sigue siendo aproximadamente la misma.
fuente
int
es 20 veces más rápido) con este código.Por los récords:
si uso
(cambiado "l--" a "l = l - 1l") el rendimiento prolongado mejora en ~ 50%
fuente
No tengo una máquina de 64 bits para probar, pero la diferencia bastante grande sugiere que hay más que el código de bytes un poco más largo en el trabajo.
Veo tiempos muy cercanos para long / int (4400 vs 4800ms) en mi 1.7.0_45 de 32 bits.
Esto es solo una suposición , pero sospecho firmemente que es el efecto de una penalización por desalineación de la memoria. Para confirmar / negar la sospecha, intente agregar un public static int dummy = 0; antes de la declaración de i. Eso reducirá 4 bytes en el diseño de la memoria y puede alinearlo correctamente para un mejor rendimiento.Confirmado que no está causando el problema.EDITAR:
El razonamiento detrás de esto es que la VM no puede reordenar los campos en su tiempo libre agregando relleno para una alineación óptima, ya que eso puede interferir con JNI(No es el caso).fuente