2012-01-19 12 views
12

Estoy jugando con un poco de código calculando el tiempo necesario para calcular algunos códigos Java para tener una idea de la eficiencia o ineficiencia de algunas de las funcionalidades de Java. Al hacerlo, ahora estoy atascado con un efecto realmente extraño. Simplemente no puedo explicarlo. Tal vez alguien de ustedes me puede ayudar a entenderlo.Java efficiency

public class PerformanceCheck { 

public static void main(String[] args) { 
    List<PerformanceCheck> removeList = new LinkedList<PerformanceCheck>(); 

    int maxTimes = 1000000000; 

    for (int i=0;i<10;i++) { 
     long time = System.currentTimeMillis(); 

     for (int times=0;times<maxTimes;times++) { 
      // PERFORMANCE CHECK BLOCK START 

      if (removeList.size() > 0) { 
       testFunc(3); 
      } 

      // PERFORMANCE CHECK BLOCK END 
     } 

     long timeNow = System.currentTimeMillis(); 
     System.out.println("time: " + (timeNow - time)); 
    } 
} 

private static boolean testFunc(int test) { 
    return 5 > test; 
} 

} 

A partir de este se traduce en un tiempo de cálculo relativamente largo (recuerda removeList está vacía, por lo TestFunc ni siquiera se llama):

time: 2328 
time: 2223 
... 

Al reemplazar algo de la combinación de removeList.size()> 0 y testFunc (3) con cualquier otra cosa tiene mejores resultados. Por ejemplo:

... 
if (removeList.size() == 0) { 
    testFunc(3); 
} 
... 

Resultados en (TestFunc se llama cada vez):

time: 8 
time: 7 
time: 0 
time: 0 

Incluso llamando a ambas funciones independientes unos de otros resultados en el tiempo de cálculo más baja:

... 
if (removeList.size() == 0); 
    testFunc(3); 
... 

resultado:

time: 6 
time: 5 
time: 0 
time: 0 
... 

Solo esta combinación particular en mi ejemplo inicial toma tanto tiempo. Esto me irrita y realmente me gustaría entenderlo. ¿Qué tiene de especial?

Gracias.

Adición:

Cambio TestFunc() en el primer ejemplo

if (removeList.size() > 0) { 
       testFunc(times); 
} 

a otra cosa, como

private static int testFunc2(int test) { 
    return 5*test; 
} 

Va resultar en ser rápido otra vez.

+0

Y ejecutó esta prueba más de una vez, y en diferentes pedidos ¿no? El orden no debería importar, pero solo para estar seguro. Además, ¿obtiene (aproximadamente) los mismos resultados con nanoTime como se sugiere a continuación? – prelic

+5

Este tipo de micro-benchmark es una muy mala idea. También tenga en cuenta que el JIT puede tomar varias decisiones en momentos arbitrarios y optimizar su código completamente, si se da cuenta de que realmente no está sucediendo nada. –

+1

Debe usar [System.nanoTime()] (http://docs.oracle.com/javase/6/docs/api/java/lang/System.html#nanoTime%28%29) para medir la ejecución del código en Java . Es más preciso más discusión [en esta pregunta] (http://stackoverflow.com/questions/351565/system-currenttimemillis-vs-system-nanotime) – paislee

Respuesta

0

Estos puntos de referencia son difíciles ya que los compiladores son tan inteligentes. Una suposición: dado que se ignora el resultado de testFunc(), el compilador podría estar optimizándolo por completo. Agregar un contador, algo así como

if (testFunc(3)) 
    counter++; 

Y, sólo por la minuciosidad, hacer un System.out.println(counter) al final.

+0

Pero eso haría que la primera versión sea más rápida, pero no lo es. –

+2

Podría ser el pedido, según lo sugerido por @prelic. Por alguna razón, JIT no optimizó la llamada la primera vez, pero se dio cuenta la segunda vez. – user949300

+0

Lo hice a nivel local y no cambia las cosas. –

3

Eso es realmente sorprendente. El bytecode generado es idéntico, excepto el condicional, que es ifle frente a ifne.

Los resultados son mucho más razonables si apaga el JIT con -Xint. La segunda versión es 2 veces más lenta. Entonces tiene que ver con lo que la optimización JIT.

Supongo que puede optimizar el cheque en el segundo caso pero no en el primero (por el motivo que sea). Aunque significa que hace el trabajo de la función, perder ese condicional hace que las cosas sean mucho más rápidas. Evita los puestos de tuberías y todo eso.

+0

El problema es que también depende de testFunc(). Intercambie el testFunc() de la primera versión con cualquier otra cosa, por ejemplo: \t private static int testFunc2 (int test) { \t \t return 5 * test; \t} Esto dará como resultado el menor tiempo de cálculo. –

1

Bueno, me alegro de no tener que lidiar con las optimizaciones de rendimiento de Java. Lo intenté yo mismo con Java JDK 7 64-Bit. Los resultados son arbitrarios;).No hace ninguna diferencia qué listas estoy usando o si guardo en caché el resultado del tamaño() antes de ingresar al ciclo. Además, aniquilar por completo la función de prueba casi no hace diferencia (por lo que tampoco puede ser un hit de predicción de bifurcación). Los indicadores de optimización mejoran el rendimiento pero son tan arbitrarios.

La única consecuencia lógica aquí es que el compilador JIT a veces puede optimizar la declaración (que no es tan difícil de ser cierto), pero parece bastante arbitraria. Una de las muchas razones por las que prefiero idiomas como C++, donde el comportamiento es al menos determinista, incluso si a veces es arbitrario.

Por cierto, en el último Eclipse, como siempre lo fue en Windows, la ejecución de este código a través de IDE "Run" (sin depuración) es 10 veces más lento que ejecutarlo desde la consola, tanto de eso ...

+0

+1 para determinista pero arbitrario –

1

Cuando el compilador de tiempo de ejecución puede averiguar testFunc evalúa a una constante, creo que no evalúa el ciclo, lo que explica la aceleración.

Cuando la condición es removeList.size() == 0 se evalúa la función testFunc(3) a una constante. Cuando la condición es removeList.size() != 0, el código interno nunca se evalúa, por lo que no se puede acelerar. Puede modificar el código de la siguiente manera:

for (int times = 0; times < maxTimes; times++) { 
      testFunc(); // Removing this call makes the code slow again! 
      if (removeList.size() != 0) { 
       testFunc(); 
      } 
     } 

private static boolean testFunc() { 
    return testFunc(3); 
} 

Cuando testFunc() no se llama inicialmente, el compilador en tiempo de ejecución no se da cuenta de que testFunc() se evalúa como una constante, por lo que no puede optimizar el bucle.

Ciertas funciones como

private static int testFunc2(int test) { 
    return 5*test; 
} 

el compilador probable trata de pre-optimize (antes de la ejecución), pero aparentemente no para el caso de un parámetro se pasa como un número entero y se evaluaron en un condicional.

Su punto de referencia vuelve momentos como

time: 107 
time: 106 
time: 0 
time: 0 
... 

lo que sugiere que se necesitan 2 iteraciones del lazo externo para el compilador en tiempo de ejecución para terminar la optimización. La compilación con la bandera -server probablemente devolverá todos los 0 en el punto de referencia.

2

Si bien no está directamente relacionado con esta pregunta, esta es la forma correcta de microcomprobar el código utilizando Caliper. A continuación se muestra una versión modificada de su código para que se ejecute con Caliper. Los bucles internos tuvieron que modificarse para que la VM no los optimizara. Es sorprendentemente inteligente al darse cuenta de que no estaba pasando nada.

También hay muchos matices cuando se compara el código de Java. Escribí sobre algunos de los problemas que encontré en Java Matrix Benchmark, por ejemplo, cómo el historial pasado puede afectar los resultados actuales. Evitará muchos de esos problemas utilizando Caliper.

  1. http://code.google.com/p/caliper/
  2. Benchmarking issues with Java Matrix Benchmark

    public class PerformanceCheck extends SimpleBenchmark { 
    
    public int timeFirstCase(int reps) { 
        List<PerformanceCheck> removeList = new LinkedList<PerformanceCheck>(); 
        removeList.add(new PerformanceCheck()); 
        int ret = 0; 
    
        for(int i = 0; i < reps; i++) { 
         if (removeList.size() > 0) { 
          if(testFunc(i)) 
           ret++; 
         } 
        } 
    
        return ret; 
    } 
    
    public int timeSecondCase(int reps) { 
        List<PerformanceCheck> removeList = new LinkedList<PerformanceCheck>(); 
        removeList.add(new PerformanceCheck()); 
        int ret = 0; 
    
        for(int i = 0; i < reps; i++) { 
         if (removeList.size() == 0) { 
          if(testFunc(i)) 
           ret++; 
         } 
        } 
    
        return ret; 
    } 
    
    private static boolean testFunc(int test) { 
        return 5 > test; 
    } 
    
    public static void main(String[] args) { 
        Runner.main(PerformanceCheck.class, args); 
    } 
    } 
    

SALIDA:

0% Scenario{vm=java, trial=0, benchmark=FirstCase} 0.60 ns; σ=0.00 ns @ 3 trials 
50% Scenario{vm=java, trial=0, benchmark=SecondCase} 1.92 ns; σ=0.22 ns @ 10 trials 

benchmark ns linear runtime 
FirstCase 0.598 ========= 
SecondCase 1.925 ============================== 

vm: java 
trial: 0 
+0

No creo que la pregunta sea cómo microbenchmark pero explicando el comportamiento observado. Pero lo golpeas en el camino; es JIT optimizando algo de formas inesperadas. –

+0

Sí, estoy de acuerdo. Pensé que podría señalar el camino correcto, ya que incluso después de que se explique el código, aún habría fluctuaciones extrañas debido a la configuración del punto de referencia. –

+0

Gracias por la punta con Caliper. Lo usaré en el futuro. Todavía queda la pregunta por qué Java actúa así.En su ejemplo, cambió el comportamiento agregando un Objeto a la lista. Ahora la cláusula if de 'timeSecondCase' devuelve falso. Y sigue siendo la única versión en la que no se llama a testFunc(), que es más lento. –

1

Los tiempos son poco realistas rápido por iteración.Esto significa que el JIT ha detectado que su código no hace nada y lo ha eliminado. Los cambios sutiles pueden confundir el JIT y no puede determinar que el código no haga nada y lleva algo de tiempo.

Si cambia la prueba para hacer algo marginalmente útil, la diferencia desaparecerá.