Est-il coûteux d'utiliser des blocs try-catch même si une exception n'est jamais levée?

189

Nous savons qu'il est coûteux d'attraper des exceptions. Mais est-il également coûteux d'utiliser un bloc try-catch en Java même si une exception n'est jamais levée?

J'ai trouvé la question / réponse Stack Overflow Pourquoi les blocs d'essai sont-ils chers? , mais c'est pour .NET .

jsedano
la source
30
Il n'y a vraiment aucun sens à cette question. Try..catch a un but très précis. Si vous en avez besoin, vous en avez besoin. Dans tous les cas, à quoi sert un essai sans prise?
JohnFx
49
try { /* do stuff */ } finally { /* make sure to release resources */ }est légal et utile
A4L
4
Ce coût doit être mis en balance avec les avantages. Cela n'est pas seul. Dans tous les cas, le coût est relatif, et jusqu'à ce que vous sachiez que vous ne pouvez pas le faire, il est logique d'utiliser la méthode la plus évidente plutôt que de ne pas faire quelque chose, car cela pourrait vous faire gagner une milliseconde ou deux en une heure. exécution du programme.
Joel
4
J'espère que cela ne mène pas à une situation de type "réinventons les codes d'erreur" ...
mikołak
6
@SAFX: avec Java7, vous pouvez même vous débarrasser du finallybloc en utilisant untry-with-resources
a_horse_with_no_name

Réponses:

201

tryn'a presque aucun frais du tout. Au lieu de faire le travail de configuration tryau moment de l'exécution, les métadonnées du code sont structurées au moment de la compilation de sorte que lorsqu'une exception est levée, il effectue maintenant une opération relativement coûteuse consistant à remonter la pile et à voir s'il tryexiste des blocs qui pourraient intercepter cela. exception. Du point de vue d'un profane, trypeut tout aussi bien être gratuit. C'est en fait une exception qui vous coûte - mais à moins que vous ne lanciez des centaines ou des milliers d'exceptions, vous ne remarquerez toujours pas le coût.


trya quelques coûts mineurs associés. Java ne peut pas effectuer certaines optimisations sur le code d'un trybloc, ce qu'il ferait autrement. Par exemple, Java réorganise souvent les instructions dans une méthode pour la rendre plus rapide - mais Java doit également garantir que si une exception est levée, l'exécution de la méthode est observée comme si ses instructions, telles qu'écrites dans le code source, étaient exécutées dans l'ordre jusqu'à une ligne.

Parce que dans un trybloc une exception peut être levée (à n'importe quelle ligne du bloc try! Certaines exceptions sont levées de manière asynchrone, comme en appelant stopun Thread (qui est obsolète), et même en plus, OutOfMemoryError peut se produire presque n'importe où) et pourtant il peut être intercepté et le code continue à s'exécuter par la suite dans la même méthode, il est plus difficile de raisonner sur les optimisations qui peuvent être faites, donc elles sont moins susceptibles de se produire. (Quelqu'un devrait programmer le compilateur pour les faire, raisonner et garantir l'exactitude, etc. Ce serait une grande douleur pour quelque chose censé être «exceptionnel») Mais encore une fois, en pratique, vous ne remarquerez pas des choses comme ça.

Patashu
la source
2
Certaines exceptions sont lancées de manière asynchrone , elles ne sont pas asynchrones mais lancées dans des points sûrs. et cet essai partiel a des coûts mineurs associés. Java ne peut pas effectuer certaines optimisations sur le code d'un bloc try, ce qu'il ferait autrement, il a besoin d'une référence sérieuse. À un moment donné, le code est très probablement dans le bloc try / catch. Il est peut-être vrai que le bloc try / catch serait plus difficile à intégrer et à construire un treillis approprié pour le résultat, mais la partie avec le réarrangement est ambiguë.
bestsss
2
Un try...finallyblocage sans catchempêche- t-il également certaines optimisations?
dajood le
5
@Patashu "C'est en fait lancer l'exception qui vous coûte" Techniquement, lancer l'exception n'est pas cher; instancier l' Exceptionobjet est ce qui prend la plupart du temps.
Austin D
72

Mesurons-le, d'accord?

public abstract class Benchmark {

    final String name;

    public Benchmark(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1;
            } while (duration < 100000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    public static void main(String[] args) throws Exception {
        Benchmark[] benchmarks = {
            new Benchmark("try") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        try {
                            x += i;
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    return x;
                }
            }, new Benchmark("no try") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x += i;
                    }
                    return x;
                }
            }
        };
        for (Benchmark bm : benchmarks) {
            System.out.println(bm);
        }
    }
}

Sur mon ordinateur, cela imprime quelque chose comme:

try     0.598 ns
no try  0.601 ns

Au moins dans cet exemple trivial, l'instruction try n'a eu aucun impact mesurable sur les performances. N'hésitez pas à mesurer les plus complexes.

De manière générale, je recommande de ne pas s'inquiéter du coût des performances des constructions de langage tant que vous n'avez pas la preuve d'un problème de performances réel dans votre code. Ou comme Donald Knuth mis il: « l' optimisation prématurée est la racine de tous les maux ».

meriton
la source
4
alors que try / no try est très probablement le même sur la plupart des JVM, le microbenchmark est terriblement imparfait.
bestsss
2
pas mal de niveaux: vous voulez dire que les résultats sont calculés en moins de 1ns? Le code compilé supprimera à la fois le try / catch ET la boucle (la somme des nombres de 1 à n est une somme de progression arithmétique triviale). Même si le code contient try / finally, le compilateur peut le prouver, il n'y a rien à y jeter. Le code abstrait n'a que 2 sites d'appel et il sera cloné et incorporé. Il y a plus de cas, il suffit de chercher quelques articles sur le microbenchmark et si vous décidez d'écrire un microbenchmark, vérifiez toujours l'assemblage généré.
bestsss
3
Les temps rapportés sont par itération de la boucle. Comme une mesure ne sera utilisée que si elle a un temps total écoulé> 0,1 seconde (ou 2 milliards d'itérations, ce qui n'était pas le cas ici), je trouve que votre affirmation selon laquelle la boucle a été supprimée dans son intégralité est difficile à croire - car si la boucle a été supprimée, qu'est-ce qui a pris 0,1 seconde pour s'exécuter?
meriton
... et en effet, selon -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly, à la fois la boucle et l'addition qu'elle contient sont présentes dans le code natif généré. Et non, les méthodes abstraites ne sont pas intégrées, car leur appelant n'est pas seulement compilé dans le temps (probablement, car il n'est pas invoqué suffisamment de fois).
meriton
Comment écrire un micro-benchmark correct en Java: stackoverflow.com/questions/504103/…
Vadzim
47

try/ catchpeut avoir un impact sur les performances. En effet, cela empêche JVM d'effectuer certaines optimisations. Joshua Bloch, dans «Effective Java», a déclaré ce qui suit:

• Placer du code dans un bloc try-catch inhibe certaines optimisations que les implémentations JVM modernes pourraient sinon effectuer.

Evgeniy Dorofeev
la source
25
"cela empêche JVM de faire quelques optimisations" ...? Pourriez-vous élaborer du tout?
The Kraken
5
@Le code Kraken à l'intérieur des blocs try (généralement? Toujours?) Ne peut pas être réorganisé avec du code en dehors des blocs try, par exemple.
Patashu
3
Notez que la question était de savoir si «c'est cher», pas si «cela a un impact sur les performances».
mikołak
3
ajouté un extrait de Effective Java , et c'est la bible de java, bien sûr; à moins qu'il n'y ait une référence, l'extrait ne dit rien. Pratiquement tout code en java est dans try / finally à un certain niveau.
bestsss
29

Oui, comme les autres l'ont dit, un trybloc inhibe certaines optimisations sur les {}personnages qui l'entourent. En particulier, l'optimiseur doit supposer qu'une exception peut se produire à tout moment dans le bloc, il n'y a donc aucune garantie que les instructions seront exécutées.

Par exemple:

    try {
        int x = a + b * c * d;
        other stuff;
    }
    catch (something) {
        ....
    }
    int y = a + b * c * d;
    use y somehow;

Sans le try, la valeur calculée à attribuer xpourrait être enregistrée en tant que "sous-expression commune" et réutilisée pour être affectée y. Mais à cause de la, tryil n'y a aucune garantie que la première expression ait jamais été évaluée, donc l'expression doit être recalculée. Ce n'est généralement pas un gros problème dans le code «en ligne droite», mais cela peut être significatif dans une boucle.

Il convient de noter, cependant, que cela s'applique UNIQUEMENT au code JITCed. javac n'effectue qu'une quantité impressionnante d'optimisation, et il n'y a aucun coût pour l'interpréteur de bytecode pour entrer / quitter un trybloc. (Aucun bytecode n'est généré pour marquer les limites du bloc.)

Et pour les meilleurs:

public class TryFinally {
    public static void main(String[] argv) throws Throwable {
        try {
            throw new Throwable();
        }
        finally {
            System.out.println("Finally!");
        }
    }
}

Production:

C:\JavaTools>java TryFinally
Finally!
Exception in thread "main" java.lang.Throwable
        at TryFinally.main(TryFinally.java:4)

sortie javap:

C:\JavaTools>javap -c TryFinally.class
Compiled from "TryFinally.java"
public class TryFinally {
  public TryFinally();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Throwable;
    Code:
       0: new           #2                  // class java/lang/Throwable
       3: dup
       4: invokespecial #3                  // Method java/lang/Throwable."<init>":()V
       7: athrow
       8: astore_1
       9: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: ldc           #5                  // String Finally!
      14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      17: aload_1
      18: athrow
    Exception table:
       from    to  target type
           0     9     8   any
}

Pas de "GOTO".

Hot Licks
la source
Il n'y a pas de bytecodes générés pour marquer les limites du bloc, ce n'est pas nécessairement le cas - cela nécessite que GOTO quitte le bloc, sinon il tombera dans le catch/finallycadre.
bestsss
@bestsss - Même si un GOTO est généré (ce qui n'est pas donné), le coût est minime, et c'est loin d'être un "marqueur" pour une limite de bloc - GOTO peut être généré pour de nombreuses constructions.
Hot Licks
Je n'ai jamais mentionné le coût, mais aucun bytecode n'est généré, c'est une fausse déclaration. C'est tout. En fait, il n'y a pas de blocs dans le bytecode, les trames ne sont pas égales aux blocs.
bestsss
Il n'y aura pas de GOTO si l'essai tombe directement dans le final, et il existe d'autres scénarios où il n'y aura pas de GOTO. Le fait est qu'il n'y a rien dans l'ordre des bytecodes "enter try" / "exit try".
Hot Licks
Il n'y aura pas de GOTO si l'essai tombe directement dans le Enfin - Faux! il n'y a pas finallyde bytecode, c'est try/catch(Throwable any){...; throw any;}And It a une instruction catch w / a frame et Throwable qui DOIT ÊTRE définie (non nulle) et ainsi de suite. Pourquoi essayez-vous d'argumenter sur le sujet, vous pouvez vérifier au moins un bytecode? La directive actuelle pour impl. of finally copie les blocs et évite la section goto (implément précédent) mais les bytecodes doivent être copiés en fonction du nombre de points de sortie.
bestsss
8

Pour comprendre pourquoi les optimisations ne peuvent pas être effectuées, il est utile de comprendre les mécanismes sous-jacents. L'exemple le plus succinct que j'ai pu trouver a été implémenté dans les macros C à l' adresse : http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

#include <stdio.h>
#include <setjmp.h>
#define TRY do{ jmp_buf ex_buf__; switch( setjmp(ex_buf__) ){ case 0: while(1){
#define CATCH(x) break; case x:
#define FINALLY break; } default:
#define ETRY } }while(0)
#define THROW(x) longjmp(ex_buf__, x)

Les compilateurs ont souvent du mal à déterminer si un saut peut être localisé vers X, Y et Z, donc ils sautent les optimisations dont ils ne peuvent garantir la sécurité, mais l'implémentation elle-même est plutôt légère.

technosaure
la source
4
Ces macros C que vous avez trouvées pour try / catch ne sont pas équivalentes à Java ou à l'implémentation C #, qui émettent 0 instructions d'exécution.
Patashu
L'implémentation java est trop étendue pour l'inclure dans son intégralité, il s'agit d'une implémentation simplifiée dans le but de comprendre l'idée de base de la manière dont les exceptions peuvent être implémentées. Dire qu'il émet 0 instructions d'exécution est trompeur. Par exemple, une simple classcastexception étend runtimeexception qui étend l'exception qui étend le jetable qui implique: grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/ ... ... C'est comme dire qu'un switch-case en C est gratuit si un seul cas est utilisé, il y a encore une petite surcharge de démarrage.
technosaurus
1
@Patashu Tous ces bits précompilés doivent encore être chargés au démarrage, qu'ils soient ou non utilisés. Il n'y a aucun moyen de savoir s'il y aura une exception de mémoire insuffisante pendant l'exécution au moment de la compilation - c'est pourquoi elles sont appelées exceptions d'exécution - sinon ce seraient des avertissements / erreurs du compilateur, donc non, cela n'optimise pas tout. , tout le code pour les gérer est inclus dans le code compilé et a un coût de démarrage.
technosaurus
2
Je ne peux pas parler de C. En C # et Java, try est implémenté en ajoutant des métadonnées, pas du code. Lorsqu'un bloc try est entré, rien n'est exécuté pour l'indiquer - lorsqu'une exception est levée, la pile est déroulée et les métadonnées vérifiées pour les gestionnaires de ce type d'exception (coûteux).
Patashu
1
Oui, j'ai en fait implémenté un interpréteur Java et un compilateur de bytecode statique et travaillé sur un JITC ultérieur (pour IBM iSeries) et je peux vous dire qu'il n'y a rien qui "marque" l'entrée / la sortie de la plage try dans les bytecodes, mais plutôt les plages sont identifiées dans un tableau séparé. L'interpréteur ne fait rien de spécial pour une plage d'essai (jusqu'à ce qu'une exception soit levée). Un JITC (ou un compilateur de bytecode statique) doit être conscient des limites pour supprimer les optimisations comme indiqué précédemment.
Hot Licks
8

Encore un microbenchmark ( source ).

J'ai créé un test dans lequel je mesure la version de code try-catch et no-try-catch en fonction d'un pourcentage d'exception. Un pourcentage de 10% signifie que 10% des cas de test avaient une division par zéro cas. Dans une situation, il est géré par un bloc try-catch, dans l'autre par un opérateur conditionnel. Voici mon tableau de résultats:

OS: Windows 8 6.2 x64
JVM: Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 23.25-b01
Pourcentage | Résultat (essayer / si, ns)   
    0% | 88/90   
    1% | 89/87    
    10% | 86/97    
    90% | 85/83   

Ce qui dit qu'il n'y a aucune différence significative entre ces cas.

Andrey Chaschev
la source
3

J'ai trouvé la capture de NullPointException assez chère. Pour les opérations de 1,2 km, le temps était de 200 ms et 12 ms lorsque je l'ai géré de la même manière, if(object==null)ce qui était une bonne amélioration pour moi.

Mateusz Kaflowski
la source