web-dev-qa-db-fra.com

Quelle partie du lancement d'une Exception coûte cher?

En Java, l’utilisation de throw/catch dans la logique quand il n’ya pas réellement d’erreur est généralement une mauvaise idée (en partie) car lancer et rattraper une exception coûte cher, et le faire plusieurs fois dans une boucle est généralement beaucoup plus lent que d’autres. structures de contrôle qui ne comportent pas d'exceptions de lancement.

Ma question est la suivante: le coût est-il lié au lancer/au rattrapage lui-même ou à la création de l'objet Exception (puisqu'il contient beaucoup d'informations d'exécution, y compris la pile d'exécution)?

En d'autres termes, si je le fais

Exception e = new Exception();

mais ne le lancez pas, est-ce la plus grande partie du coût du lancer, ou est-ce que la manipulation du lancer et de la capture est coûteuse?

Je ne demande pas si l'insertion de code dans un bloc try/catch augmente le coût d'exécution de ce code, mais si l'interception de l'exception est la partie la plus chère ou la création (en appelant le constructeur) d'exception est la partie la plus chère .

Une autre façon de poser cette question est la suivante: si je créais un cas d'Exception et le jetais et le rattrapais encore et encore, est-ce que cela serait beaucoup plus rapide que de créer une nouvelle Exception chaque fois que je lance?

249
Martin Carney

La création d'un objet d'exception n'est pas plus coûteuse que la création d'autres objets ordinaires. Le coût principal est masqué dans la méthode native fillInStackTrace qui parcourt la pile d'appels et collecte toutes les informations requises pour créer une trace de pile: classes, noms de méthodes, numéros de ligne, etc.

Le mythe concernant les coûts d'exception élevés provient du fait que la plupart des constructeurs Throwable appellent implicitement fillInStackTrace. Cependant, il existe un constructeur pour créer un Throwable sans trace de pile. Cela vous permet de créer des objets jetables très rapides à instancier. Une autre façon de créer des exceptions légères consiste à remplacer fillInStackTrace.


Maintenant, qu'en est-il de levant une exception?
En fait, cela dépend de l'endroit où une exception levée est capturée .

S'il est pris dans la même méthode (ou, plus précisément, dans le même contexte, puisque le contexte peut inclure plusieurs méthodes en raison d'inline), alors throw est aussi simple et rapide que goto (bien sûr, après la compilation de JIT).

Toutefois, si un bloc catch se situe quelque part plus profondément dans la pile, JVM doit alors dérouler les trames de la pile, ce qui peut prendre beaucoup plus de temps. Cela prend encore plus de temps, si des blocs ou méthodes synchronized sont impliqués, car le déroulement implique la libération des moniteurs appartenant aux cadres de pile supprimés.


Je pourrais confirmer les affirmations ci-dessus à l'aide de critères de référence appropriés, mais heureusement, je n'ai pas besoin de le faire, car tous les aspects sont déjà parfaitement décrits dans le post de l'ingénieur de performance de HotSpot, Alexey Shipilev: La performance exceptionnelle de Lil 'Exception .

253
apangin

La première opération dans la plupart des constructeurs Throwable consiste à à remplir la trace de pile, , où se trouve l'essentiel de la dépense.

Il existe cependant un constructeur protégé avec un indicateur pour désactiver la trace de la pile. Ce constructeur est également accessible lors de l'extension de Exception. Si vous créez un type d'exception personnalisé, vous pouvez éviter la création de trace de pile et obtenir de meilleures performances au détriment de moins d'informations.

Si vous créez une seule exception d'un type quelconque par des moyens normaux, vous pouvez la relancer plusieurs fois sans avoir à surcharger le suivi de la pile. Cependant, sa trace de pile indiquera où elle a été construite, et non pas où elle a été lancée dans un cas particulier.

Les versions actuelles de Java tentent d’optimiser la création de traces. Le code natif est appelé pour remplir la trace de la pile, qui enregistre la trace dans une structure native plus légère. Les objets Java correspondants StackTraceElement sont créés paresseusement à partir de cet enregistrement uniquement lorsque les getStackTrace(), printStackTrace(), ou d'autres méthodes nécessitant la trace sont appelées.

Si vous éliminez la génération de traces de pile, l’autre coût principal consiste à dérouler la pile entre le lancer et le rattraper. Moins il y a de trames intermédiaires rencontrées avant que l'exception ne soit interceptée, plus vite ce sera.

Concevez votre programme de manière à ce que les exceptions ne soient levées que dans des cas vraiment exceptionnels, et les optimisations de ce type sont difficiles à justifier.

72
erickson

Theres une bonne rédaction sur les exceptions ici.

http://shipilev.net/blog/2014/exceptional-performance/

La conclusion étant que la construction de la trace de pile et le déroulement de la pile sont des pièces coûteuses. Le code ci-dessous tire parti d'une fonctionnalité de 1.7 permettant d'activer et de désactiver les traces de pile. Nous pouvons ensuite utiliser cela pour voir quel type de coûts différents scénarios ont

Vous trouverez ci-dessous les timings pour la création d'objet uniquement. J'ai ajouté String pour que vous puissiez voir que, sans la pile, la création d'un objet JavaException et d'un String n'a pratiquement aucune différence. Avec l’écriture de pile activée, la différence est spectaculaire, c’est-à-dire qu’au moins un ordre de grandeur est plus lent.

Time to create million String objects: 41.41 (ms)
Time to create million JavaException objects with    stack: 608.89 (ms)
Time to create million JavaException objects without stack: 43.50 (ms)

Ce qui suit montre combien de temps il a fallu pour revenir d’un million de fois à une profondeur donnée.

|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%)|
|   16|           1428|             243| 588 (%)|
|   15|           1763|             393| 449 (%)|
|   14|           1746|             390| 448 (%)|
|   13|           1703|             384| 443 (%)|
|   12|           1697|             391| 434 (%)|
|   11|           1707|             410| 416 (%)|
|   10|           1226|             197| 622 (%)|
|    9|           1242|             206| 603 (%)|
|    8|           1251|             207| 604 (%)|
|    7|           1213|             208| 583 (%)|
|    6|           1164|             206| 565 (%)|
|    5|           1134|             205| 553 (%)|
|    4|           1106|             203| 545 (%)|
|    3|           1043|             192| 543 (%)| 

Ce qui suit est presque certainement une grossière sur la simplification ...

Si nous prenons une profondeur de 16 avec l'écriture de pile sur, alors que la création d'un objet prend environ 40% du temps, la trace de pile proprement dite en représente la grande majorité. Environ 93% de l'instanciation de l'objet JavaException est due à la trace de la pile en cours de prise. Cela signifie que, dans ce cas, dérouler la pile prend l'autre moitié du temps.

Lorsque nous désactivons la pile, la création d'objets de trace représente une fraction beaucoup plus petite, à savoir 20%, et le dépilage de la pile représente désormais 80% du temps.

Dans les deux cas, le déroulement de la pile prend une grande partie du temps total.

public class JavaException extends Exception {
  JavaException(String reason, int mode) {
    super(reason, null, false, false);
  }
  JavaException(String reason) {
    super(reason);
  }

  public static void main(String[] args) {
    int iterations = 1000000;
    long create_time_with    = 0;
    long create_time_without = 0;
    long create_string = 0;
    for (int i = 0; i < iterations; i++) {
      long start = System.nanoTime();
      JavaException jex = new JavaException("testing");
      long stop  =  System.nanoTime();
      create_time_with += stop - start;

      start = System.nanoTime();
      JavaException jex2 = new JavaException("testing", 1);
      stop = System.nanoTime();
      create_time_without += stop - start;

      start = System.nanoTime();
      String str = new String("testing");
      stop = System.nanoTime();
      create_string += stop - start;

    }
    double interval_with    = ((double)create_time_with)/1000000;
    double interval_without = ((double)create_time_without)/1000000;
    double interval_string  = ((double)create_string)/1000000;

    System.out.printf("Time to create %d String objects: %.2f (ms)\n", iterations, interval_string);
    System.out.printf("Time to create %d JavaException objects with    stack: %.2f (ms)\n", iterations, interval_with);
    System.out.printf("Time to create %d JavaException objects without stack: %.2f (ms)\n", iterations, interval_without);

    JavaException jex = new JavaException("testing");
    int depth = 14;
    int i = depth;
    double[] with_stack    = new double[20];
    double[] without_stack = new double[20];

    for(; i > 0 ; --i) {
      without_stack[i] = jex.timerLoop(i, iterations, 0)/1000000;
      with_stack[i]    = jex.timerLoop(i, iterations, 1)/1000000;
    }
    i = depth;
    System.out.printf("|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%%)|\n");
    for(; i > 0 ; --i) {
      double ratio = (with_stack[i] / (double) without_stack[i]) * 100;
      System.out.printf("|%5d| %14.0f| %15.0f| %2.0f (%%)| \n", i + 2, with_stack[i] , without_stack[i], ratio);
      //System.out.printf("%d\t%.2f (ms)\n", i, ratio);
    }
  }
 private int thrower(int i, int mode) throws JavaException {
    ExArg.time_start[i] = System.nanoTime();
    if(mode == 0) { throw new JavaException("without stack", 1); }
    throw new JavaException("with stack");
  }
  private int catcher1(int i, int mode) throws JavaException{
    return this.stack_of_calls(i, mode);
  }
  private long timerLoop(int depth, int iterations, int mode) {
    for (int i = 0; i < iterations; i++) {
      try {
        this.catcher1(depth, mode);
      } catch (JavaException e) {
        ExArg.time_accum[depth] += (System.nanoTime() - ExArg.time_start[depth]);
      }
    }
    //long stop = System.nanoTime();
    return ExArg.time_accum[depth];
  }

  private int bad_method14(int i, int mode) throws JavaException  {
    if(i > 0) { this.thrower(i, mode); }
    return i;
  }
  private int bad_method13(int i, int mode) throws JavaException  {
    if(i == 13) { this.thrower(i, mode); }
    return bad_method14(i,mode);
  }
  private int bad_method12(int i, int mode) throws JavaException{
    if(i == 12) { this.thrower(i, mode); }
    return bad_method13(i,mode);
  }
  private int bad_method11(int i, int mode) throws JavaException{
    if(i == 11) { this.thrower(i, mode); }
    return bad_method12(i,mode);
  }
  private int bad_method10(int i, int mode) throws JavaException{
    if(i == 10) { this.thrower(i, mode); }
    return bad_method11(i,mode);
  }
  private int bad_method9(int i, int mode) throws JavaException{
    if(i == 9) { this.thrower(i, mode); }
    return bad_method10(i,mode);
  }
  private int bad_method8(int i, int mode) throws JavaException{
    if(i == 8) { this.thrower(i, mode); }
    return bad_method9(i,mode);
  }
  private int bad_method7(int i, int mode) throws JavaException{
    if(i == 7) { this.thrower(i, mode); }
    return bad_method8(i,mode);
  }
  private int bad_method6(int i, int mode) throws JavaException{
    if(i == 6) { this.thrower(i, mode); }
    return bad_method7(i,mode);
  }
  private int bad_method5(int i, int mode) throws JavaException{
    if(i == 5) { this.thrower(i, mode); }
    return bad_method6(i,mode);
  }
  private int bad_method4(int i, int mode) throws JavaException{
    if(i == 4) { this.thrower(i, mode); }
    return bad_method5(i,mode);
  }
  protected int bad_method3(int i, int mode) throws JavaException{
    if(i == 3) { this.thrower(i, mode); }
    return bad_method4(i,mode);
  }
  private int bad_method2(int i, int mode) throws JavaException{
    if(i == 2) { this.thrower(i, mode); }
    return bad_method3(i,mode);
  }
  private int bad_method1(int i, int mode) throws JavaException{
    if(i == 1) { this.thrower(i, mode); }
    return bad_method2(i,mode);
  }
  private int stack_of_calls(int i, int mode) throws JavaException{
    if(i == 0) { this.thrower(i, mode); }
    return bad_method1(i,mode);
  }
}

class ExArg {
  public static long[] time_start;
  public static long[] time_accum;
  static {
     time_start = new long[20];
     time_accum = new long[20];
  };
}

Les cadres de pile dans cet exemple sont minuscules comparés à ce que vous trouverez normalement.

Vous pouvez jeter un coup d'œil au bytecode en utilisant javap

javap -c -v -constants JavaException.class

c'est à dire c'est pour la méthode 4 ...

   protected int bad_method3(int, int) throws JavaException;
flags: ACC_PROTECTED
Code:
  stack=3, locals=3, args_size=3
     0: iload_1       
     1: iconst_3      
     2: if_icmpne     12
     5: aload_0       
     6: iload_1       
     7: iload_2       
     8: invokespecial #6                  // Method thrower:(II)I
    11: pop           
    12: aload_0       
    13: iload_1       
    14: iload_2       
    15: invokespecial #17                 // Method bad_method4:(II)I
    18: ireturn       
  LineNumberTable:
    line 63: 0
    line 64: 12
  StackMapTable: number_of_entries = 1
       frame_type = 12 /* same */

Exceptions:
  throws JavaException
25
Harry

La création de Exception avec une trace de pile null prend environ autant de temps que le bloc throw et try-catch ensemble. Cependant, remplir la trace de la pile prend en moyenne 5 fois plus longtemps.

J'ai créé le repère suivant pour démontrer l'impact sur les performances. J'ai ajouté le -Djava.compiler=NONE à la configuration d'exécution pour désactiver l'optimisation du compilateur. Pour mesurer l'impact de la création de la trace de pile, j'ai étendu la classe Exception afin de tirer parti du constructeur sans pile:

class NoStackException extends Exception{
    public NoStackException() {
        super("",null,false,false);
    }
}

Le code de référence est le suivant:

public class ExceptionBenchmark {

    private static final int NUM_TRIES = 100000;

    public static void main(String[] args) {

        long throwCatchTime = 0, newExceptionTime = 0, newObjectTime = 0, noStackExceptionTime = 0;

        for (int i = 0; i < 30; i++) {
            throwCatchTime += throwCatchLoop();
            newExceptionTime += newExceptionLoop();
            newObjectTime += newObjectLoop();
            noStackExceptionTime += newNoStackExceptionLoop();
        }

        System.out.println("throwCatchTime = " + throwCatchTime / 30);
        System.out.println("newExceptionTime = " + newExceptionTime / 30);
        System.out.println("newStringTime = " + newObjectTime / 30);
        System.out.println("noStackExceptionTime = " + noStackExceptionTime / 30);

    }

    private static long throwCatchLoop() {
        Exception ex = new Exception(); //Instantiated here
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            try {
                throw ex; //repeatedly thrown
            } catch (Exception e) {

                // do nothing
            }
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newExceptionLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Exception e = new Exception();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newObjectLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Object o = new Object();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newNoStackExceptionLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            NoStackException e = new NoStackException();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

}

Sortie:

throwCatchTime = 19
newExceptionTime = 77
newObjectTime = 3
noStackExceptionTime = 15

Cela implique que créer une NoStackException est environ aussi coûteux que de lancer plusieurs fois le même Exception. Il montre également que créer une Exception et remplir sa trace de pile prend environ 4x plus longtemps.

12
Austin D

Cette partie de la question ...

Une autre façon de poser cette question est la suivante: si je créais un cas d'Exception et le jetais et le rattrapais encore et encore, est-ce que cela serait beaucoup plus rapide que de créer une nouvelle Exception chaque fois que je lance?

Semble demander si créer une exception et la mettre en cache quelque part améliore les performances. Oui. Cela revient à désactiver la pile en cours d'écriture lors de la création d'un objet, car cela a déjà été fait.

Ce sont des timings que j'ai eu, s'il vous plaît lire l'avertissement après ceci ...

|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%)|
|   16|            193|             251| 77 (%)| 
|   15|            390|             406| 96 (%)| 
|   14|            394|             401| 98 (%)| 
|   13|            381|             385| 99 (%)| 
|   12|            387|             370| 105 (%)| 
|   11|            368|             376| 98 (%)| 
|   10|            188|             192| 98 (%)| 
|    9|            193|             195| 99 (%)| 
|    8|            200|             188| 106 (%)| 
|    7|            187|             184| 102 (%)| 
|    6|            196|             200| 98 (%)| 
|    5|            197|             193| 102 (%)| 
|    4|            198|             190| 104 (%)| 
|    3|            193|             183| 105 (%)| 

Bien sûr, le problème avec ceci est que votre trace de pile pointe maintenant vers l'endroit où vous avez instancié l'objet et non d'où il a été jeté.

4
Harry

En utilisant la réponse de @ AustinD comme point de départ, j'ai fait quelques ajustements. Code en bas.

En plus d'ajouter le cas où une instance d'exception est levée à plusieurs reprises, j'ai également désactivé l'optimisation du compilateur afin d'obtenir des résultats de performances précis. J'ai ajouté -Djava.compiler=NONE aux arguments VM, comme indiqué dans cette réponse . (Dans Eclipse, éditez la configuration d'exécution → Arguments pour définir cet argument VM)

Les resultats:

new Exception + throw/catch = 643.5
new Exception only          = 510.7
throw/catch only            = 115.2
new String (benchmark)      = 669.8

Donc, créer une exception coûte environ 5 fois plus cher que le lancer + l’attraper. En supposant que le compilateur n'optimise pas une grande partie des coûts.

À titre de comparaison, voici le même test sans désactiver l'optimisation:

new Exception + throw/catch = 382.6
new Exception only          = 379.5
throw/catch only            = 0.3
new String (benchmark)      = 15.6

Code:

public class ExceptionPerformanceTest {

    private static final int NUM_TRIES = 1000000;

    public static void main(String[] args) {

        double numIterations = 10;

        long exceptionPlusCatchTime = 0, excepTime = 0, strTime = 0, throwTime = 0;

        for (int i = 0; i < numIterations; i++) {
            exceptionPlusCatchTime += exceptionPlusCatchBlock();
            excepTime += createException();
            throwTime += catchBlock();
            strTime += createString();
        }

        System.out.println("new Exception + throw/catch = " + exceptionPlusCatchTime / numIterations);
        System.out.println("new Exception only          = " + excepTime / numIterations);
        System.out.println("throw/catch only            = " + throwTime / numIterations);
        System.out.println("new String (benchmark)      = " + strTime / numIterations);

    }

    private static long exceptionPlusCatchBlock() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            try {
                throw new Exception();
            } catch (Exception e) {
                // do nothing
            }
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long createException() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Exception e = new Exception();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long createString() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Object o = new String("" + i);
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long catchBlock() {
        Exception ex = new Exception(); //Instantiated here
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            try {
                throw ex; //repeatedly thrown
            } catch (Exception e) {
                // do nothing
            }
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }
}
3
Martin Carney