2012-10-12 218 views
13

Estoy usando el siguiente código para probar qué tan lento es un bloque try. Para mi sorpresa, el bloque try lo hace más rápido. ¿Por qué?¿Por qué agregar un bloque try hace que el programa sea más rápido?

public class Test { 
    int value; 

    public int getValue() { 
     return value; 
    } 

    public void reset() { 
     value = 0; 
    } 

    // Calculates without exception 
    public void method1(int i) { 
     value = ((value + i)/i) << 1; 
     // Will never be true 
     if ((i & 0xFFFFFFF) == 1000000000) { 
      System.out.println("You'll never see this!"); 
     } 
    } 

    public static void main(String[] args) { 
     int i; 
     long l; 
     Test t = new Test(); 

     l = System.currentTimeMillis(); 
     t.reset(); 
     for (i = 1; i < 100000000; i++) { 
      t.method1(i); 
     } 
     l = System.currentTimeMillis() - l; 
     System.out.println("method1 took " + l + " ms, result was " 
       + t.getValue()); 

     // using a try block 
     l = System.currentTimeMillis(); 
     t.reset(); 
     for (i = 1; i < 100000000; i++) { 
      try { 
       t.method1(i); 
      } catch (Exception e) { 

      } 
     } 

     l = System.currentTimeMillis() - l; 
     System.out.println("method1 with try block took " + l + " ms, result was " 
       + t.getValue()); 
    } 
} 

Mi máquina ejecuta Windows 7 de 64 bits y JDK7 de 64 bits. Obtuve el siguiente resultado:

method1 took 914 ms, result was 2 
method1 with try block took 789 ms, result was 2 

Y he ejecutado el código muchas veces y cada vez obtuve casi el mismo resultado.

Actualización:

Aquí está el resultado de ejecutar la prueba de diez veces en un MacBook Pro, Java 6. try-catch hace que el método más rápido, al igual que en las ventanas.

method1 took 895 ms, result was 2 
method1 with try block took 783 ms, result was 2 
-------------------------------------------------- 
method1 took 943 ms, result was 2 
method1 with try block took 803 ms, result was 2 
-------------------------------------------------- 
method1 took 867 ms, result was 2 
method1 with try block took 745 ms, result was 2 
-------------------------------------------------- 
method1 took 856 ms, result was 2 
method1 with try block took 744 ms, result was 2 
-------------------------------------------------- 
method1 took 862 ms, result was 2 
method1 with try block took 744 ms, result was 2 
-------------------------------------------------- 
method1 took 859 ms, result was 2 
method1 with try block took 765 ms, result was 2 
-------------------------------------------------- 
method1 took 937 ms, result was 2 
method1 with try block took 767 ms, result was 2 
-------------------------------------------------- 
method1 took 861 ms, result was 2 
method1 with try block took 744 ms, result was 2 
-------------------------------------------------- 
method1 took 858 ms, result was 2 
method1 with try block took 744 ms, result was 2 
-------------------------------------------------- 
method1 took 858 ms, result was 2 
method1 with try block took 749 ms, result was 2 
+1

Esto está fuera de el alcance de la pregunta, pero debe usar 'System.nanoTime' para comparar los datos. Lea [System.currentTimeMillis vs System.nanoTime] (http://stackoverflow.com/q/351565/1065197). –

+2

@RahulAgrawal Cambié el código y obtuve el mismo resultado. –

+2

Hice muchas pruebas sobre el código de OP, y confirmo su descubrimiento. –

Respuesta

2

He hecho algunas experimentaciones.

Para empezar, confirmo totalmente el hallazgo de OP. Incluso si elimina el primer bucle o cambia la excepción por una totalmente irrelevante, la captura de prueba, siempre que no agregue una bifurcación reiniciando la excepción, hace que el código sea más rápido. El código es aún más rápido si realmente tiene que atrapar una excepción (si hace que el bucle comience en 0 en lugar de 1, por ejemplo).

Mi "explicación" es que JIT son máquinas de optimización salvaje y que a veces funcionan mejor que en otras ocasiones, de forma que no se puede entender generalmente sin un estudio muy específico en el nivel JIT. Hay muchas cosas posibles que pueden cambiar (uso de registros, por ejemplo).

This is globally what was found in a very similar case with a C# JIT.

En cualquier caso, Java está optimizado para try-catch. Como siempre existe la posibilidad de una excepción, realmente no agrega mucha ramificación añadiendo un try-catch, por lo que no es sorprendente no encontrar el segundo bucle más largo que el primero.

19

Cuando tiene múltiples bucles de larga ejecución en el mismo método, uno puede activar la optimización de todo el método con resultados impredecibles en el segundo bucle. Una forma de evitar esto es;

  • para dar cada bucle su propio método
  • ejecutar las pruebas múltiples veces para comprobar el resultado es re-producible
  • ejecutar la prueba durante 2 - 10 segundos.

Verá algunas variaciones y, a veces, los resultados no son concluyentes. es decir, la variación es mayor que la diferencia.

public class Test { 
    int value; 

    public int getValue() { 
     return value; 
    } 

    public void reset() { 
     value = 0; 
    } 

    // Calculates without exception 
    public void method1(int i) { 
     value = ((value + i)/i) << 1; 
     // Will never be true 
     if ((i & 0xFFFFFFF) == 1000000000) { 
      System.out.println("You'll never see this!"); 
     } 
    } 

    public static void main(String[] args) { 
     Test t = new Test(); 
     for (int i = 0; i < 5; i++) { 
      testWithTryCatch(t); 
      testWithoutTryCatch(t); 
     } 
    } 

    private static void testWithoutTryCatch(Test t) { 
     t.reset(); 
     long l = System.currentTimeMillis(); 
     for (int j = 0; j < 10; j++) 
      for (int i = 1; i <= 100000000; i++) 
       t.method1(i); 

     l = System.currentTimeMillis() - l; 
     System.out.println("without try/catch method1 took " + l + " ms, result was " + t.getValue()); 
    } 

    private static void testWithTryCatch(Test t) { 
     t.reset(); 
     long l = System.currentTimeMillis(); 
     for (int j = 0; j < 10; j++) 
      for (int i = 1; i <= 100000000; i++) 
       try { 
        t.method1(i); 
       } catch (Exception ignored) { 
       } 

     l = System.currentTimeMillis() - l; 
     System.out.println("with try/catch method1 took " + l + " ms, result was " + t.getValue()); 
    } 
} 

impresiones

with try/catch method1 took 9723 ms, result was 2 
without try/catch method1 took 9456 ms, result was 2 
with try/catch method1 took 9672 ms, result was 2 
without try/catch method1 took 9476 ms, result was 2 
with try/catch method1 took 8375 ms, result was 2 
without try/catch method1 took 8233 ms, result was 2 
with try/catch method1 took 8337 ms, result was 2 
without try/catch method1 took 8227 ms, result was 2 
with try/catch method1 took 8163 ms, result was 2 
without try/catch method1 took 8565 ms, result was 2 

partir de estos resultados, podría parecer que con el try/catch es ligeramente más lento, pero no siempre.

funcionar en Windows 7, Xeon E5450 con Java 7 actualización 7.

+0

En mi máquina, 'testWithoutTryCatch()' se vuelve más rápido desde la tercera vez, siempre. –

+0

¿Qué actualización de Java 7 tienes? –

+0

Actualización 7 de Java 7 de 64 bits - "1.7.0_07". –

5

lo probé con Calibre microbenchmark y realmente no podía ver una diferencia.

Aquí está el código:

public class TryCatchBenchmark extends SimpleBenchmark { 

    private int value; 

    public void setUp() { 
     value = 0; 
    } 

    // Calculates without exception 
    public void method1(int i) { 
     value = ((value + i)/i) << 1; 
     // Will never be true 
     if ((i & 0xFFFFFFF) == 1000000000) { 
      System.out.println("You'll never see this!"); 
     } 
    } 

    public void timeWithoutTryCatch(int reps) { 
     for (int i = 1; i < reps; i++) { 
      this.method1(i); 
     } 
    } 

    public void timeWithTryCatch(int reps) { 
     for (int i = 1; i < reps; i++) { 
      try { 
       this.method1(i); 
      } catch (Exception ignore) { 
      } 
     } 
    } 

    public static void main(String[] args) { 
     new Runner().run(TryCatchBenchmark.class.getName()); 
    } 
} 

Y aquí está el resultado:

 
0% Scenario{vm=java, trial=0, benchmark=WithoutTryCatch} 8,23 ns; σ=0,03 ns @ 3 trials 
50% Scenario{vm=java, trial=0, benchmark=WithTryCatch} 8,13 ns; σ=0,03 ns @ 3 trials 

     benchmark ns linear runtime 
WithoutTryCatch 8,23 ============================== 
    WithTryCatch 8,13 ============================= 

Si intercambiar el orden de las funciones (para conseguir que se ejecute en orden inverso) el resultado es:

 
0% Scenario{vm=java, trial=0, benchmark=WithTryCatch} 8,21 ns; σ=0,05 ns @ 3 trials 
50% Scenario{vm=java, trial=0, benchmark=WithoutTryCatch} 8,14 ns; σ=0,03 ns @ 3 trials 

     benchmark ns linear runtime 
    WithTryCatch 8,21 ============================== 
WithoutTryCatch 8,14 ============================= 

Yo diría que son básicamente lo mismo.

+0

+1 Uso de Caliper Microbenchmark. – Ryan

1

Para evitar cualquier optimización oculta o caché que pueda realizar la JVM y el sistema operativo, primero desarrollé dos programas de inicialización Java, TryBlock y NoTryBlock, donde su diferencia es usar un bloque de prueba o no. Estos dos programas semilla se usarán para generar diferentes programas para no permitir que JVM o OS hagan una optimización oculta. En cada prueba, se generará y compilará un nuevo programa de Java, y repetí la prueba 10 veces.

Según mi experimento, ejecutar sin try block toma 9779.3 ms en promedio mientras se ejecuta con try block lleva 9775.9ms: una diferencia de 3.4ms (o 0.035%) en su tiempo promedio de ejecución, que puede verse como ruido. Esto indica que usar un bloque de prueba de vacío (por nulo, me refiero a excepción distinta del puntero nulo no existe ninguna posible excepción) o no parece tener impacto en el tiempo de ejecución.

La prueba se ejecuta en la misma máquina Linux (CPU 2392MHz) y en la versión java "1.6.0_24".

A continuación es mi script para generar programas de pruebas basado en los programas de semillas:

for i in `seq 1 10`; do 
    echo "NoTryBlock$i" 
    cp NoTryBlock.java NoTryBlock$i.java 
    find . -name "NoTryBlock$i.java" -print | xargs sed -i "s/NoTryBlock/NoTryBlock$i/g"; 
    javac NoTryBlock$i.java; 
    java NoTryBlock$i 
    rm NoTryBlock$i.* -f; 
done 

for i in `seq 1 10`; do 
    echo "TryBlock$i" 
    cp TryBlock.java TryBlock$i.java 
    find . -name "TryBlock$i.java" -print | xargs sed -i "s/TryBlock/TryBlock$i/g"; 
    javac TryBlock$i.java; 
    java TryBlock$i 
    rm TryBlock$i.* -f; 
done 

Éstos son los programas de semillas, primero es el NoTryBlock.java

import java.util.*; 
import java.lang.*; 

public class NoTryBlock { 
    int value; 

    public int getValue() { 
     return value; 
    } 

    public void reset() { 
     value = 0; 
    } 

    // Calculates without exception 
    public void method1(int i) { 
     value = ((value + i)/i) << 1; 
     // Will never be true 
     if ((i & 0xFFFFFFF) == 1000000000) { 
      System.out.println("You'll never see this!"); 
     } 
    } 

    public static void main(String[] args) { 
     int i, j; 
     long l; 
     NoTryBlock t = new NoTryBlock(); 

     // using a try block 
     l = System.currentTimeMillis(); 
     t.reset(); 
     for (j = 1; j < 10; ++j) { 
      for (i = 1; i < 100000000; i++) { 
       t.method1(i); 
      } 
     } 
     l = System.currentTimeMillis() - l; 
     System.out.println(
      "method1 with try block took " + l + " ms, result was " 
       + t.getValue()); 
    } 
} 

el segundo es el TryBlock.java, que utiliza un try-block en la llamada a la función de método:

import java.util.*; 
import java.lang.*; 

public class TryBlock { 
    int value; 

    public int getValue() { 
     return value; 
    } 

    public void reset() { 
     value = 0; 
    } 

    // Calculates without exception 
    public void method1(int i) { 
     value = ((value + i)/i) << 1; 
     // Will never be true 
     if ((i & 0xFFFFFFF) == 1000000000) { 
      System.out.println("You'll never see this!"); 
     } 
    } 

    public static void main(String[] args) { 
     int i, j; 
     long l; 
     TryBlock t = new TryBlock(); 

     // using a try block 
     l = System.currentTimeMillis(); 
     t.reset(); 
     for (j = 1; j < 10; ++j) { 
      for (i = 1; i < 100000000; i++) { 
      try { 
       t.method1(i); 
      } catch (Exception e) { 
      } 
      } 
     } 
     l = System.currentTimeMillis() - l; 
     System.out.println(
      "method1 with try block took " + l + " ms, result was " 
       + t.getValue()); 
    } 
} 

A continuación se muestra el diff de mis dos programas de semillas, y se puede ver, excepto el nombre de la clase, el bloque try es su única diferencia:

$ diff TryBlock.java NoTryBlock.java 
4c4 
<  public class TryBlock { 
--- 
>  public class NoTryBlock { 
27c27 
<    TryBlock t = new TryBlock(); 
--- 
>    NoTryBlock t = new NoTryBlock(); 
34d33 
<     try { 
36,37d34 
<     } catch (Exception e) { 
<     } 
42c39 
<     "method1 with try block took " + l + " ms, result was " 
--- 
>     "method1 without try block took " + l + " ms, result was " 

A continuación se muestra la salida:

method1 without try block took,9732,ms, result was 2 
method1 without try block took,9756,ms, result was 2 
method1 without try block took,9845,ms, result was 2 
method1 without try block took,9794,ms, result was 2 
method1 without try block took,9758,ms, result was 2 
method1 without try block took,9733,ms, result was 2 
method1 without try block took,9763,ms, result was 2 
method1 without try block took,9893,ms, result was 2 
method1 without try block took,9761,ms, result was 2 
method1 without try block took,9758,ms, result was 2 

method1 with try block took,9776,ms, result was 2 
method1 with try block took,9751,ms, result was 2 
method1 with try block took,9767,ms, result was 2 
method1 with try block took,9726,ms, result was 2 
method1 with try block took,9779,ms, result was 2 
method1 with try block took,9797,ms, result was 2 
method1 with try block took,9845,ms, result was 2 
method1 with try block took,9784,ms, result was 2 
method1 with try block took,9787,ms, result was 2 
method1 with try block took,9747,ms, result was 2 
Cuestiones relacionadas