Quels sont les effets des exceptions sur les performances en Java?

496

Question: La gestion des exceptions en Java est-elle réellement lente?

La sagesse conventionnelle, ainsi que de nombreux résultats de Google, disent qu'une logique exceptionnelle ne doit pas être utilisée pour un flux de programme normal en Java. Deux raisons sont généralement données,

  1. il est vraiment lent - même un ordre de grandeur plus lent que le code normal (les raisons données varient),

et

  1. c'est désordonné car les gens s'attendent à ce que seules les erreurs soient traitées dans un code exceptionnel.

Cette question porte sur # 1.

À titre d'exemple, cette page décrit la gestion des exceptions Java comme «très lente» et met en relation la lenteur avec la création de la chaîne de message d'exception - «cette chaîne est ensuite utilisée pour créer l'objet d'exception qui est levé. Ce n'est pas rapide». L'article sur la gestion efficace des exceptions en Java indique que "la raison en est due à l'aspect de création d'objet de la gestion des exceptions, ce qui rend par conséquent le lancement d'exceptions intrinsèquement lent". Une autre raison est que la génération de trace de pile est ce qui la ralentit.

Mes tests (en utilisant Java 1.6.0_07, Java HotSpot 10.0, sur Linux 32 bits), indiquent que la gestion des exceptions n'est pas plus lente que le code normal. J'ai essayé d'exécuter une méthode dans une boucle qui exécute du code. À la fin de la méthode, j'utilise un booléen pour indiquer s'il faut retourner ou lancer . De cette façon, le traitement réel est le même. J'ai essayé d'exécuter les méthodes dans des ordres différents et de faire la moyenne de mes temps de test, pensant qu'il s'agissait peut-être de l'échauffement de la JVM. Dans tous mes tests, le lancer a été au moins aussi rapide que le retour, sinon plus rapide (jusqu'à 3,1% plus rapide). Je suis complètement ouvert à la possibilité que mes tests aient été incorrects, mais je n'ai rien vu sur le chemin de l'exemple de code, des comparaisons de tests ou des résultats au cours des deux dernières années qui montrent que la gestion des exceptions en Java est réellement lent.

Ce qui m'amène dans cette voie, c'est une API que je devais utiliser et qui lançait des exceptions dans le cadre de la logique de contrôle normale. Je voulais les corriger dans leur utilisation, mais maintenant je ne peux peut-être pas le faire. Dois-je plutôt les féliciter pour leur réflexion prospective?

Dans l'article Gestion efficace des exceptions Java dans la compilation juste à temps , les auteurs suggèrent que la présence de gestionnaires d'exceptions seuls, même si aucune exception n'est levée, est suffisante pour empêcher le compilateur JIT d'optimiser correctement le code, le ralentissant ainsi . Je n'ai pas encore testé cette théorie.

John Ellinwood
la source
8
Je sais que vous ne posiez pas de questions sur 2), mais vous devez vraiment reconnaître que l'utilisation d'une exception pour le déroulement du programme n'est pas meilleure que l'utilisation de GOTO. Certaines personnes défendent les gotos, d'autres défendraient ce dont vous parlez, mais si vous demandez à quelqu'un qui a mis en œuvre et maintenu l'une ou l'autre pendant un certain temps, ils vous diront que les deux sont difficiles à maintenir les pratiques de conception (et maudiront probablement le nom de la personne qui pensait être assez intelligente pour prendre la décision de les utiliser).
Bill K
80
Bill, affirmant que l'utilisation d'exceptions pour le flux de programme n'est pas meilleure que l'utilisation de GOTO n'est pas mieux que de prétendre que l'utilisation de conditions et de boucles pour le flux de programme n'est pas meilleure que l'utilisation de GOTO. C'est un hareng rouge. Expliquez-vous. Les exceptions peuvent et sont utilisées efficacement pour le déroulement du programme dans d'autres langues. Le code Python idiomatique utilise régulièrement des exceptions, par exemple. Je peux et ai maintenu du code qui utilise des exceptions de cette façon (pas Java cependant), et je ne pense pas qu'il y ait quelque chose de mal en soi.
mmalone
14
@mmalone utilisant des exceptions pour un flux de contrôle normal est une mauvaise idée en Java car le choix du paradigme a été fait de cette façon . Lisez Bloch EJ2 - il déclare clairement que, citation, (point 57) exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow- en donnant une explication complète et détaillée des raisons. Et c'est lui qui a écrit Java lib. C'est donc lui qui définit le contrat d'API des classes. / Je suis d'accord avec Bill K sur celui-ci.
8
@ OndraŽižka Si un cadre le fait (utiliser les exceptions dans un état non exceptionnel), il est défectueux et cassé par conception, rompant le contrat de classe d'exception de la langue. Tout simplement parce que certaines personnes écrivent du code moche ne le rend pas moins moche.
8
Nul autre que le créateur de stackoverflow.com ne se trompe sur les exceptions. La règle d'or du développement logiciel n'est jamais de rendre le simple complexe et lourd. Il écrit: "Il est vrai que ce qui devrait être un simple programme de 3 lignes s'épanouit souvent en 48 lignes lorsque vous effectuez une bonne vérification des erreurs, mais c'est la vie, ..." C'est une recherche de pureté, pas de simplicité.
sf_jeff

Réponses:

345

Cela dépend de la façon dont les exceptions sont implémentées. La manière la plus simple est d'utiliser setjmp et longjmp. Cela signifie que tous les registres du CPU sont écrits dans la pile (ce qui prend déjà un certain temps) et éventuellement d'autres données doivent être créées ... tout cela se produit déjà dans l'instruction try. L'instruction throw doit dérouler la pile et restaurer les valeurs de tous les registres (et éventuellement d'autres valeurs dans la machine virtuelle). Donc, try et throw sont également lents, ce qui est assez lent, mais si aucune exception n'est levée, la sortie du bloc try ne prend aucun temps dans la plupart des cas (car tout est placé sur la pile qui se nettoie automatiquement si la méthode existe).

Sun et d'autres ont reconnu que cela est peut-être sous-optimal et bien sûr les machines virtuelles deviennent de plus en plus rapides au fil du temps. Il existe une autre façon d'implémenter des exceptions, ce qui rend l'essai lui-même rapide comme l'éclair (en fait, rien ne se passe pour essayer du tout en général - tout ce qui doit se produire est déjà fait lorsque la classe est chargée par la machine virtuelle) et cela rend le lancer pas aussi lent . Je ne sais pas quelle JVM utilise cette nouvelle et meilleure technique ...

... mais écrivez-vous en Java afin que votre code ne s'exécute plus tard que sur une seule machine virtuelle Java sur un système spécifique? Étant donné que si elle peut jamais fonctionner sur une autre plate-forme ou toute autre version JVM (éventuellement d'un autre fournisseur), qui dit qu'ils utilisent également l'implémentation rapide? Le rapide est plus compliqué que le lent et n'est pas facilement possible sur tous les systèmes. Vous voulez rester portable? Alors ne comptez pas sur les exceptions rapides.

Cela fait également une grande différence ce que vous faites dans un bloc d'essai. Si vous ouvrez un bloc try et n'appelez jamais aucune méthode à partir de ce bloc try, le bloc try sera ultra rapide, car le JIT peut alors traiter un lancer comme un simple goto. Il n'a pas besoin de sauvegarder l'état de la pile ni de dérouler la pile si une exception est levée (il suffit de passer aux gestionnaires de capture). Cependant, ce n'est pas ce que vous faites habituellement. Habituellement, vous ouvrez un bloc try et appelez ensuite une méthode qui pourrait lever une exception, non? Et même si vous utilisez simplement le bloc try dans votre méthode, de quel type de méthode s'agit-il, qui n'appelle aucune autre méthode? Va-t-il simplement calculer un nombre? Alors pourquoi avez-vous besoin d'exceptions? Il existe des moyens beaucoup plus élégants de réguler le flux des programmes. Pour à peu près tout autre chose que des mathématiques simples,

Voir le code de test suivant:

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!");
        }
    }

    // Could in theory throw one, but never will
    public void method2(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            throw new Exception();
        }
    }

    // This one will regularly throw one
    public void method3(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
        // an AND operation between two integers. The size of the number plays
        // no role. AND on 32 BIT always ANDs all 32 bits
        if ((i & 0x1) == 1) {
            throw new Exception();
        }
    }

    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()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method2(i);
            } catch (Exception e) {
                System.out.println("You'll never see this!");
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method2 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method3(i);
            } catch (Exception e) {
                // Do nothing here, as we will get here
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method3 took " + l + " ms, result was " + t.getValue()
        );
    }
}

Résultat:

method1 took 972 ms, result was 2
method2 took 1003 ms, result was 2
method3 took 66716 ms, result was 2

Le ralentissement du bloc try est trop faible pour exclure les facteurs de confusion tels que les processus d'arrière-plan. Mais le bloc de capture a tout tué et a ralenti 66 fois!

Comme je l'ai dit, le résultat ne sera pas si mauvais si vous mettez try / catch et throw all dans la même méthode (method3), mais c'est une optimisation JIT spéciale sur laquelle je ne me fierais pas. Et même en utilisant cette optimisation, le lancer est encore assez lent. Je ne sais donc pas ce que vous essayez de faire ici, mais il y a certainement une meilleure façon de le faire que d'utiliser try / catch / throw.

Mecki
la source
7
Excellente réponse, mais je voudrais simplement ajouter qu'à ma connaissance, System.nanoTime () doit être utilisé pour mesurer les performances, pas System.currentTimeMillis ().
Simon Forsberg
10
@ SimonAndréForsberg nanoTime()nécessite Java 1.5 et je n'avais que Java 1.4 disponible sur le système que j'ai utilisé pour écrire le code ci-dessus. De plus, cela ne joue pas un rôle énorme dans la pratique. La seule différence entre les deux est que l'une est nanoseconde l'autre millisecondes et nanoTimen'est pas influencée par les manipulations d'horloge (qui ne sont pas pertinentes, sauf si vous ou le processus système modifiez l'horloge système exactement au moment où le code de test s'exécute). En règle générale, vous avez raison, bien nanoTimesûr , est le meilleur choix.
Mecki
2
Il faut vraiment noter que votre test est un cas extrême. Vous montrez un très petit succès de performance pour le code avec un trybloc, mais non throw. Votre throwtest est levée des exceptions 50% du temps il passe par la try. C'est clairement une situation où l'échec n'est pas exceptionnel . Le réduire à seulement 10% réduit massivement les performances. Le problème avec ce type de test est qu'il encourage les gens à cesser complètement d'utiliser les exceptions. L'utilisation d'exceptions, pour une gestion exceptionnelle des cas, fonctionne bien mieux que ce que montre votre test.
Nate
1
@Nate Tout d'abord, j'ai dit très clairement que tout cela dépend de la façon dont les exceptions sont mises en œuvre. Je testais juste UNE implémentation spécifique, mais il y en a beaucoup et Oracle peut en choisir une entièrement différente à chaque version. Deuxièmement, si les exceptions ne sont qu'exceptionnelles, ce qu'elles sont généralement, bien sûr l'impact est plus petit, c'est tellement évident, que je ne pense vraiment pas qu'il faille le signaler explicitement et donc je ne comprends pas du tout votre point ici. Et troisièmement, à l'exception d'une mauvaise utilisation, tout le monde est d'accord là-dessus, donc les utiliser avec beaucoup de soin est une très bonne chose.
Mecki
4
@Glide Un lancer n'est pas comme un nettoyage return. Il laisse une méthode quelque part au milieu du corps, peut-être même au milieu d'une opération (qui n'a jusqu'à présent été complétée que de 50%) et le catchbloc peut contenir 20 images de pile vers le haut (une méthode a un trybloc, appelant la méthode 1, qui appelle method2, qui appelle mehtod3, ..., et dans method20 au milieu d'une opération, une exception est levée). La pile doit être déroulée de 20 images vers le haut, toutes les opérations inachevées doivent être annulées (les opérations ne doivent pas être à moitié effectuées) et les registres du processeur doivent être dans un état propre. Tout cela prend du temps.
Mecki
256

Pour info, j'ai prolongé l'expérience de Mecki:

method1 took 1733 ms, result was 2
method2 took 1248 ms, result was 2
method3 took 83997 ms, result was 2
method4 took 1692 ms, result was 2
method5 took 60946 ms, result was 2
method6 took 25746 ms, result was 2

Les 3 premiers sont les mêmes que ceux de Mecki (mon ordinateur portable est évidemment plus lent).

La méthode 4 est identique à la méthode 3, sauf qu'elle crée un new Integer(1)plutôt que de le faire throw new Exception().

method5 est comme method3 sauf qu'il crée le new Exception()sans le lancer.

method6 est similaire à method3, sauf qu'il lève une exception pré-créée (une variable d'instance) plutôt que d'en créer une nouvelle.

En Java, une grande partie des dépenses liées au lancement d'une exception est le temps passé à collecter la trace de pile, qui se produit lorsque l'objet d'exception est créé. Le coût réel de levée de l'exception, bien qu'il soit important, est considérablement inférieur au coût de création de l'exception.

Hot Licks
la source
49
+1 Votre réponse résout le problème principal - le temps nécessaire pour dérouler et suivre la pile, et secondairement le lancement de l'erreur. J'aurais choisi ceci comme réponse finale.
Ingénieur
9
agréable. ~ 70% créent l'exception, ~ 30% la lèvent. bonne info.
chaqke
1
@Basil - Vous devriez pouvoir le comprendre à partir des chiffres ci-dessus.
Hot Licks
2
@HotLicks et c'est exactement pourquoi il est important de dire quelle version de Java a été utilisée dans le post
Thorbjørn Ravn Andersen
3
Nous pouvons remarquer que dans le code standard, la création et le lancement d'exceptions se produisent dans de rares cas (à l'exécution, je veux dire), si ce n'est pas le cas, soit les conditions d'exécution sont très mauvaises, soit la conception est elle-même le problème; dans les deux cas les performances ne sont pas un souci ...
Jean-Baptiste Yunès
70

Aleksey Shipilëv a fait une analyse très approfondie dans laquelle il compare les exceptions Java dans diverses combinaisons de conditions:

  • Exceptions nouvellement créées vs exceptions pré-créées
  • Trace de pile activée vs désactivée
  • Trace de pile demandée vs jamais demandée
  • Pris au plus haut niveau vs renversé à tous les niveaux vs enchaîné / enveloppé à tous les niveaux
  • Différents niveaux de profondeur de pile d'appels Java
  • Aucune optimisation inlining vs extrême inline vs paramètres par défaut
  • Champs définis par l'utilisateur lus ou non lus

Il les compare également aux performances de vérification d'un code d'erreur à différents niveaux de fréquence d'erreur.

Les conclusions (citées mot pour mot de son message) étaient les suivantes:

  1. Les exceptions vraiment exceptionnelles sont magnifiquement performantes. Si vous les utilisez comme prévu et ne communiquez les cas vraiment exceptionnels qu'au nombre écrasant de cas non exceptionnels traités par du code normal, alors l'utilisation d'exceptions est la meilleure performance.

  2. Les coûts de performance des exceptions ont deux composants principaux: la construction de la trace de la pile lorsque l'exception est instanciée et le déroulement de la pile pendant le lancement de l'exception.

  3. Les coûts de construction des traces de pile sont proportionnels à la profondeur de la pile au moment de l'instanciation d'exception. C'est déjà mauvais parce que qui sur Terre connaît la profondeur de la pile à laquelle cette méthode de lancement serait appelée? Même si vous désactivez la génération de trace de pile et / ou mettez en cache les exceptions, vous ne pouvez vous débarrasser que de cette partie du coût des performances.

  4. Les coûts de déroulement de la pile dépendent de la chance que nous avons de rapprocher le gestionnaire d'exceptions dans le code compilé. Structurer soigneusement le code pour éviter une recherche approfondie de gestionnaires d'exceptions nous aide probablement à avoir plus de chance.

  5. Si nous éliminons les deux effets, le coût de performance des exceptions est celui de la branche locale. Peu importe sa beauté, cela ne signifie pas que vous devez utiliser Exceptions comme flux de contrôle habituel, car dans ce cas, vous êtes à la merci d'optimiser le compilateur! Vous ne devez les utiliser que dans des cas vraiment exceptionnels, où la fréquence des exceptions amortit le coût malchanceux possible de la levée de l'exception réelle.

  6. La règle empirique optimiste semble être une fréquence de 10 ^ -4 pour les exceptions, ce qui est assez exceptionnel. Cela, bien sûr, dépend des poids lourds des exceptions elles-mêmes, des actions exactes prises dans les gestionnaires d'exceptions, etc.

Le résultat est que lorsqu'une exception n'est pas levée, vous ne payez pas de frais, donc lorsque la condition exceptionnelle est suffisamment rare, la gestion des exceptions est plus rapide que l'utilisation à ifchaque fois. Le post complet vaut vraiment la peine d'être lu.

Doval
la source
41

Malheureusement, ma réponse est trop longue pour être publiée ici. Alors permettez-moi de résumer ici et de vous référer à http://www.fuwjax.com/how-slow-are-java-exceptions/ pour les détails concrets.

La vraie question ici n'est pas "Quelle est la lenteur des" échecs signalés comme exceptions "par rapport au" code qui n'échoue jamais "?" comme la réponse acceptée pourrait vous le faire croire. Au lieu de cela, la question devrait être "Quelle est la lenteur des" échecs signalés en tant qu'exceptions "par rapport aux échecs signalés par d'autres moyens?" Généralement, les deux autres façons de signaler les échecs sont soit avec des valeurs sentinelles, soit avec des wrappers de résultats.

Les valeurs sentinelles sont une tentative de renvoyer une classe en cas de succès et une autre en cas d'échec. Vous pouvez y penser presque comme renvoyant une exception au lieu d'en lancer une. Cela nécessite une classe parent partagée avec l'objet de réussite, puis une vérification "instanceof" et quelques transtypages pour obtenir les informations de réussite ou d'échec.

Il se trouve qu'au risque de sécurité de type, les valeurs Sentinel sont plus rapides que les exceptions, mais seulement d'un facteur d'environ 2x. Maintenant, cela peut sembler beaucoup, mais ce 2x ne couvre que le coût de la différence de mise en œuvre. En pratique, le facteur est beaucoup plus faible car nos méthodes qui pourraient échouer sont beaucoup plus intéressantes que quelques opérateurs arithmétiques comme dans l'exemple de code ailleurs dans cette page.

Les enveloppes de résultat, d'autre part, ne sacrifient pas du tout la sécurité du type. Ils regroupent les informations de réussite et d'échec dans une seule classe. Ainsi, au lieu de "instanceof", ils fournissent un "isSuccess ()" et des getters pour les objets de réussite et d'échec. Cependant, les objets de résultat sont environ 2 fois plus lents que l'utilisation d'exceptions. Il s'avère que créer un nouvel objet wrapper à chaque fois est beaucoup plus cher que de lancer une exception parfois.

En plus de cela, le langage fourni permet d'indiquer qu'une méthode peut échouer. Il n'y a pas d'autre moyen de savoir à partir de l'API uniquement les méthodes qui devraient toujours (principalement) fonctionner et celles qui devraient signaler un échec.

Les exceptions sont plus sûres que les sentinelles, plus rapides que les objets résultants et moins surprenantes que les deux. Je ne suggère pas que try / catch remplace if / else, mais les exceptions sont le bon moyen de signaler un échec, même dans la logique métier.

Cela dit, je voudrais souligner que les deux moyens les plus fréquents d'influer considérablement sur les performances que j'ai rencontrés sont la création d'objets inutiles et de boucles imbriquées. Si vous avez le choix entre créer une exception ou ne pas créer d'exception, ne créez pas l'exception. Si vous avez le choix entre créer une exception parfois ou créer un autre objet tout le temps, créez l'exception.

Fuwjax
la source
5
J'ai décidé de tester les performances à long terme des trois implémentations par rapport à une implémentation de contrôle qui vérifie les échecs sans rapport. Le processus a un taux d'échec d'environ 4%. Une itération d'un test appelle le processus 10000 fois contre l'une des stratégies. Chaque stratégie est testée 1000 fois et les 900 dernières fois sont utilisées pour générer les statistiques. Voici les temps moyens en nanos: Contrôle 338 Exception 429 Résultat 348 Sentinel 345
Fuwjax
2
Juste pour le plaisir, j'ai désactivé fillInStackTrace dans le test d'exception. Voici les temps maintenant: Contrôle 347 Exception 351 Résultat 364 Sentinelle 355
Fuwjax
1
Fuwjax, à moins que je manque quelque chose (et j'avoue que je n'ai lu que votre article SO, pas votre article de blog), il semble que vos deux commentaires ci-dessus contredisent votre article. Je suppose que les nombres inférieurs sont meilleurs dans votre indice de référence, non? Dans ce cas, la génération d'exceptions avec fillInStackTrace activé (qui est le comportement par défaut et habituel), entraîne des performances plus lentes que les deux autres techniques que vous décrivez. Suis-je en train de manquer quelque chose, ou avez-vous réellement commenté pour réfuter votre message?
Felix GV
@Fuwjax - la façon d'éviter le choix "rock and hard place" que vous présentez ici, est de pré-allouer un objet qui représente le "succès". Habituellement, on peut également pré-allouer des objets pour les cas de défaillance courants. Ce n'est que dans les rares cas où des détails supplémentaires sont renvoyés, qu'un nouvel objet est créé. (Il s'agit de l'équivalent OO des "codes d'erreur" entiers, plus un appel séparé pour obtenir les détails de la dernière erreur - une technique qui existe depuis des décennies.)
ToolmakerSteve
@Fuwjax Donc, lever une exception ne crée pas d'objet par votre compte? Je ne suis pas sûr de comprendre ce raisonnement. Que vous leviez une exception ou renvoyiez un objet résultat, vous créez des objets. En ce sens, les objets de résultat ne sont pas plus lents que de lever une exception.
Matthias
20

J'ai étendu les réponses données par @Mecki et @incarnate , sans remplissage stacktrace pour Java.

Avec Java 7+, nous pouvons utiliser Throwable(String message, Throwable cause, boolean enableSuppression,boolean writableStackTrace). Mais pour Java6, voir ma réponse à cette question

// This one will regularly throw one
public void method4(int i) throws NoStackTraceThrowable {
    value = ((value + i) / i) << 1;
    // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
    // an AND operation between two integers. The size of the number plays
    // no role. AND on 32 BIT always ANDs all 32 bits
    if ((i & 0x1) == 1) {
        throw new NoStackTraceThrowable();
    }
}

// This one will regularly throw one
public void method5(int i) throws NoStackTraceRuntimeException {
    value = ((value + i) / i) << 1;
    // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
    // an AND operation between two integers. The size of the number plays
    // no role. AND on 32 BIT always ANDs all 32 bits
    if ((i & 0x1) == 1) {
        throw new NoStackTraceRuntimeException();
    }
}

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++) {
        try {
            t.method4(i);
        } catch (NoStackTraceThrowable e) {
            // Do nothing here, as we will get here
        }
    }
    l = System.currentTimeMillis() - l;
    System.out.println( "method4 took " + l + " ms, result was " + t.getValue() );


    l = System.currentTimeMillis();
    t.reset();
    for (i = 1; i < 100000000; i++) {
        try {
            t.method5(i);
        } catch (RuntimeException e) {
            // Do nothing here, as we will get here
        }
    }
    l = System.currentTimeMillis() - l;
    System.out.println( "method5 took " + l + " ms, result was " + t.getValue() );
}

Sortie avec Java 1.6.0_45, sur Core i7, 8 Go de RAM:

method1 took 883 ms, result was 2
method2 took 882 ms, result was 2
method3 took 32270 ms, result was 2 // throws Exception
method4 took 8114 ms, result was 2 // throws NoStackTraceThrowable
method5 took 8086 ms, result was 2 // throws NoStackTraceRuntimeException

Ainsi, les méthodes qui retournent des valeurs sont toujours plus rapides que les méthodes qui génèrent des exceptions. À mon humble avis, nous ne pouvons pas concevoir une API claire en utilisant simplement des types de retour pour les flux de réussite et d'erreur. Les méthodes qui génèrent des exceptions sans stacktrace sont 4 à 5 fois plus rapides que les exceptions normales.

Edit: NoStackTraceThrowable.java Merci @Greg

public class NoStackTraceThrowable extends Throwable { 
    public NoStackTraceThrowable() { 
        super("my special throwable", null, false, false);
    }
}
manikanta
la source
intéressant, merci. Voici la déclaration de classe manquante:public class NoStackTraceThrowable extends Throwable { public NoStackTraceThrowable() { super("my special throwable", null, false, false); } }
Greg
à beginging Vous avez écrit With Java 7+, we can usemais plus tard vous avez écrit c'est Output with Java 1.6.0_45,donc le résultat Java 6 ou 7
WBAR
1
@WBAR de Java 7, nous avons juste besoin d'utiliser le Throwableconstructeur qui a boolean writableStackTracearg. Mais cela n'est pas présent dans Java 6 et inférieur. C'est pourquoi j'ai donné une implémentation personnalisée pour Java 6 et inférieur. Le code ci-dessus est donc pour Java 6 et inférieur. Veuillez lire attentivement la 1ère ligne du 2ème para.
manikanta
@manikanta "À mon humble avis, nous ne pouvons pas concevoir une API claire en utilisant simplement des types de retour pour les flux de réussite et d'erreur." - nous le pouvons, si nous utilisons des options / résultats / peut-être comme le font de nombreuses langues.
Hejazzman
@Hejazzman, je suis d'accord. Mais Optionalou similaire est arrivé un peu tard à Java. Avant cela, nous utilisions également des objets wrapper avec des indicateurs de réussite / d'erreur. Mais cela semble être un peu hacks et ne me semble pas naturel.
manikanta
8

Il y a quelque temps, j'ai écrit une classe pour tester les performances relatives de la conversion de chaînes en entiers en utilisant deux approches: (1) appeler Integer.parseInt () et intercepter l'exception, ou (2) faire correspondre la chaîne avec une expression régulière et appeler parseInt () seulement si le match réussit. J'ai utilisé l'expression régulière de la manière la plus efficace possible (c'est-à-dire en créant les objets Pattern et Matcher avant d'intervenir dans la boucle), et je n'ai pas imprimé ou enregistré les traces de pile des exceptions.

Pour une liste de dix mille chaînes, s'il s'agissait de nombres valides, l'approche parseInt () était quatre fois plus rapide que l'approche regex. Mais si seulement 80% des chaînes étaient valides, l'expression régulière était deux fois plus rapide que parseInt (). Et si 20% étaient valides, ce qui signifie que l'exception a été levée et interceptée 80% du temps, l'expression régulière était environ vingt fois plus rapide que parseInt ().

J'ai été surpris par le résultat, étant donné que l'approche regex traite deux fois des chaînes valides: une fois pour la correspondance et de nouveau pour parseInt (). Mais lever et attraper des exceptions a plus que compensé cela. Ce genre de situation n'est pas susceptible de se produire très souvent dans le monde réel, mais si c'est le cas, vous ne devriez certainement pas utiliser la technique de capture d'exception. Mais si vous ne validez que les entrées utilisateur ou quelque chose comme ça, utilisez certainement l'approche parseInt ().

Alan Moore
la source
quelle JVM avez-vous utilisée? est-ce toujours aussi lent avec sun-jdk 6?
Benedikt Waldvogel
Je l'ai déterré et exécuté à nouveau sous JDK 1.6u10 avant de soumettre cette réponse, et ce sont les résultats que j'ai publiés.
Alan Moore
C'est très, très utile! Merci. Pour mes cas d'utilisation habituels, j'ai besoin d'analyser les entrées utilisateur (en utilisant quelque chose comme Integer.ParseInt()) et je m'attends à ce que la plupart du temps, l'entrée utilisateur soit correcte, donc pour mon cas d'utilisation, il semble que la prise de l'exception occasionnelle soit la voie à suivre .
markvgti
8

Je pense que le premier article fait référence à l'acte de traverser la pile d'appels et de créer une trace de pile comme étant la partie coûteuse, et bien que le deuxième article ne le dise pas, je pense que c'est la partie la plus coûteuse de la création d'objets. John Rose a un article où il décrit différentes techniques pour accélérer les exceptions . (Préallocation et réutilisation d'une exception, exceptions sans traces de pile, etc.)

Mais encore - je pense que cela ne devrait être considéré que comme un mal nécessaire, un dernier recours. La raison pour laquelle John fait cela est d'émuler des fonctionnalités dans d'autres langues qui ne sont pas (encore) disponibles dans la JVM. Vous ne devez PAS prendre l'habitude d'utiliser des exceptions pour le flux de contrôle. Surtout pas pour des raisons de performances! Comme vous le mentionnez vous-même au # 2, vous risquez de masquer de graves bogues dans votre code de cette manière, et il sera plus difficile à maintenir pour les nouveaux programmeurs.

Les microbenchmarks en Java sont étonnamment difficiles à obtenir correctement (on m'a dit), surtout lorsque vous entrez en territoire JIT, donc je doute vraiment que l'utilisation des exceptions soit plus rapide que le "retour" dans la vie réelle. Par exemple, je suppose que vous avez entre 2 et 5 cadres de pile dans votre test? Imaginez maintenant que votre code sera invoqué par un composant JSF déployé par JBoss. Vous pouvez maintenant avoir une trace de pile de plusieurs pages.

Peut-être pourriez-vous publier votre code de test?

Lars Westergren
la source
7

Je ne sais pas si ces sujets se rapportent, mais j'ai voulu implémenter une astuce en se basant sur la trace de la pile du thread actuel: je voulais découvrir le nom de la méthode, qui a déclenché l'instanciation à l'intérieur de la classe instanciée (oui, l'idée est folle, Je l'ai totalement abandonné). J'ai donc découvert que l'appel Thread.currentThread().getStackTrace()est extrêmement lent (en raison de la dumpThreadsméthode native qu'il utilise en interne).

Java Throwablea donc une méthode native fillInStackTrace. Je pense que le killer- catchblock décrit plus haut déclenche en quelque sorte l'exécution de cette méthode.

Mais laissez-moi vous raconter une autre histoire ...

Dans Scala, certaines fonctionnalités sont compilées dans JVM à l'aide de ControlThrowable, ce qui étend Throwableet remplace son fillInStackTracede la manière suivante:

override def fillInStackTrace(): Throwable = this

J'ai donc adapté le test ci-dessus (le nombre de cycles est diminué de dix, ma machine est un peu plus lente :):

class ControlException extends ControlThrowable

class T {
  var value = 0

  def reset = {
    value = 0
  }

  def method1(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      println("You'll never see this!")
    }
  }

  def method2(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      throw new Exception()
    }
  }

  def method3(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new Exception()
    }
  }

  def method4(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new ControlException()
    }
  }
}

class Main {
  var l = System.currentTimeMillis
  val t = new T
  for (i <- 1 to 10000000)
    t.method1(i)
  l = System.currentTimeMillis - l
  println("method1 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method2(i)
  } catch {
    case _ => println("You'll never see this")
  }
  l = System.currentTimeMillis - l
  println("method2 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method4(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method4 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method3(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method3 took " + l + " ms, result was " + t.value)

}

Les résultats sont donc:

method1 took 146 ms, result was 2
method2 took 159 ms, result was 2
method4 took 1551 ms, result was 2
method3 took 42492 ms, result was 2

Vous voyez, la seule différence entre method3et method4est qu'ils lèvent différents types d'exceptions. Yeap, method4est toujours plus lent que method1et method2, mais la différence est beaucoup plus acceptable.

incarner
la source
6

J'ai fait des tests de performances avec JVM 1.5 et l'utilisation des exceptions était au moins 2x plus lente. En moyenne: le temps d'exécution sur une méthode trivialement petite a plus que triplé (3x) avec des exceptions. Une boucle trivialement petite qui devait rattraper l'exception a vu une augmentation de 2x du temps libre.

J'ai vu des chiffres similaires dans le code de production ainsi que des micro-repères.

Les exceptions ne doivent certainement PAS être utilisées pour tout ce qui est appelé fréquemment. Lancer des milliers d'exceptions par seconde provoquerait un énorme goulot d'étranglement.

Par exemple, utiliser "Integer.ParseInt (...)" pour trouver toutes les mauvaises valeurs dans un très gros fichier texte - très mauvaise idée. (J'ai vu cette méthode utilitaire tuer les performances sur le code de production)

Utilisation d'une exception pour signaler une mauvaise valeur sur un formulaire GUI utilisateur, probablement pas si mauvaise du point de vue des performances.

Que ce soit une bonne pratique de conception ou non, j'opterais pour la règle: si l'erreur est normale / attendue, utilisez une valeur de retour. Si c'est anormal, utilisez une exception. Par exemple: lors de la lecture des entrées utilisateur, les mauvaises valeurs sont normales - utilisez un code d'erreur. En passant une valeur à une fonction utilitaire interne, les mauvaises valeurs doivent être filtrées en appelant du code - utilisez une exception.

James Schek
la source
Permettez-moi de suggérer certaines choses qui SONT bonnes à faire: Si vous avez besoin d'un nombre dans un formulaire, au lieu d'utiliser Integer.valueOf (String), vous devriez plutôt envisager d'utiliser un adaptateur d'expressions régulières. Vous pouvez précompiler et réutiliser le modèle afin que les appariements soient bon marché. Cependant, sur un formulaire GUI, avoir un isValid / validate / checkField ou ce que vous avez est probablement plus clair. De plus, avec Java 8, nous avons des monades facultatives, alors envisagez de les utiliser. (la réponse est de 9 ans, mais quand même!: p)
Haakon Løtveit
4

Des performances exceptionnelles en Java et en C # laissent beaucoup à désirer.

En tant que programmeurs, cela nous oblige à respecter la règle "les exceptions doivent être occasionnelles", simplement pour des raisons pratiques de performances.

Cependant, en tant qu'informaticiens, nous devons nous rebeller contre cet état problématique. La personne qui crée une fonction n'a souvent aucune idée de la fréquence à laquelle elle sera appelée, ni si le succès ou l'échec est plus probable. Seul l'appelant dispose de ces informations. Essayer d'éviter les exceptions conduit à des idoms d'API peu clairs où, dans certains cas, nous n'avons que des versions d'exception propres mais lentes, et dans d'autres cas, nous avons des erreurs de valeur de retour rapides mais maladroites, et dans d'autres cas nous nous retrouvons avec les deux . Le réalisateur de la bibliothèque peut devoir écrire et maintenir deux versions d'API, et l'appelant doit décider laquelle des deux versions utiliser dans chaque situation.

C'est une sorte de gâchis. Si les exceptions avaient de meilleures performances, nous pourrions éviter ces idiomes maladroits et utiliser les exceptions telles qu'elles étaient censées être utilisées ... comme une fonction de retour d'erreur structurée.

J'aimerais vraiment voir des mécanismes d'exception implémentés en utilisant des techniques plus proches des valeurs de retour, afin que nous puissions avoir des performances plus proches des valeurs de retour .. puisque c'est ce à quoi nous revenons dans un code sensible aux performances.

Voici un exemple de code qui compare les performances d'exception aux performances de valeur de retour d'erreur.

classe public TestIt {

int value;


public int getValue() {
    return value;
}

public void reset() {
    value = 0;
}

public boolean baseline_null(boolean shouldfail, int recurse_depth) {
    if (recurse_depth <= 0) {
        return shouldfail;
    } else {
        return baseline_null(shouldfail,recurse_depth-1);
    }
}

public boolean retval_error(boolean shouldfail, int recurse_depth) {
    if (recurse_depth <= 0) {
        if (shouldfail) {
            return false;
        } else {
            return true;
        }
    } else {
        boolean nested_error = retval_error(shouldfail,recurse_depth-1);
        if (nested_error) {
            return true;
        } else {
            return false;
        }
    }
}

public void exception_error(boolean shouldfail, int recurse_depth) throws Exception {
    if (recurse_depth <= 0) {
        if (shouldfail) {
            throw new Exception();
        }
    } else {
        exception_error(shouldfail,recurse_depth-1);
    }

}

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

    int ITERATION_COUNT = 100000000;


    // (0) baseline null workload
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                t.baseline_null(shoulderror,recurse_depth);
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("baseline: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);
        }
    }


    // (1) retval_error
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                if (!t.retval_error(shoulderror,recurse_depth)) {
                    failures++;
                }
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("retval_error: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);
        }
    }

    // (2) exception_error
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                try {
                    t.exception_error(shoulderror,recurse_depth);
                } catch (Exception e) {
                    failures++;
                }
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("exception_error: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);              
        }
    }
}

}

Et voici les résultats:

baseline: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 683 ms
baseline: recurse_depth 2, exception_freqeuncy 0.25 (0), time elapsed 790 ms
baseline: recurse_depth 2, exception_freqeuncy 0.5 (0), time elapsed 768 ms
baseline: recurse_depth 2, exception_freqeuncy 0.75 (0), time elapsed 749 ms
baseline: recurse_depth 2, exception_freqeuncy 1.0 (0), time elapsed 731 ms
baseline: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 923 ms
baseline: recurse_depth 5, exception_freqeuncy 0.25 (0), time elapsed 971 ms
baseline: recurse_depth 5, exception_freqeuncy 0.5 (0), time elapsed 982 ms
baseline: recurse_depth 5, exception_freqeuncy 0.75 (0), time elapsed 947 ms
baseline: recurse_depth 5, exception_freqeuncy 1.0 (0), time elapsed 937 ms
baseline: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1154 ms
baseline: recurse_depth 8, exception_freqeuncy 0.25 (0), time elapsed 1149 ms
baseline: recurse_depth 8, exception_freqeuncy 0.5 (0), time elapsed 1133 ms
baseline: recurse_depth 8, exception_freqeuncy 0.75 (0), time elapsed 1117 ms
baseline: recurse_depth 8, exception_freqeuncy 1.0 (0), time elapsed 1116 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 742 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.25 (24999999), time elapsed 743 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.5 (49999999), time elapsed 734 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.75 (99999999), time elapsed 723 ms
retval_error: recurse_depth 2, exception_freqeuncy 1.0 (99999999), time elapsed 728 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 920 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.25 (24999999), time elapsed 1121   ms
retval_error: recurse_depth 5, exception_freqeuncy 0.5 (49999999), time elapsed 1037 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.75 (99999999), time elapsed 1141   ms
retval_error: recurse_depth 5, exception_freqeuncy 1.0 (99999999), time elapsed 1130 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1218 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.25 (24999999), time elapsed 1334  ms
retval_error: recurse_depth 8, exception_freqeuncy 0.5 (49999999), time elapsed 1478 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.75 (99999999), time elapsed 1637 ms
retval_error: recurse_depth 8, exception_freqeuncy 1.0 (99999999), time elapsed 1655 ms
exception_error: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 726 ms
exception_error: recurse_depth 2, exception_freqeuncy 0.25 (24999999), time elapsed 17487   ms
exception_error: recurse_depth 2, exception_freqeuncy 0.5 (49999999), time elapsed 33763   ms
exception_error: recurse_depth 2, exception_freqeuncy 0.75 (99999999), time elapsed 67367   ms
exception_error: recurse_depth 2, exception_freqeuncy 1.0 (99999999), time elapsed 66990 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 924 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.25 (24999999), time elapsed 23775  ms
exception_error: recurse_depth 5, exception_freqeuncy 0.5 (49999999), time elapsed 46326 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.75 (99999999), time elapsed 91707 ms
exception_error: recurse_depth 5, exception_freqeuncy 1.0 (99999999), time elapsed 91580 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1144 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.25 (24999999), time elapsed 30440 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.5 (49999999), time elapsed 59116   ms
exception_error: recurse_depth 8, exception_freqeuncy 0.75 (99999999), time elapsed 116678 ms
exception_error: recurse_depth 8, exception_freqeuncy 1.0 (99999999), time elapsed 116477 ms

La vérification et la propagation des valeurs de retour ajoutent un coût par rapport à l'appel de référence nulle, et ce coût est proportionnel à la profondeur des appels. À une profondeur de chaîne d'appels de 8, la version de vérification de la valeur de retour d'erreur était environ 27% plus lente que la version de base qui ne vérifiait pas les valeurs de retour.

Les performances d'exception, en comparaison, ne sont pas fonction de la profondeur d'appel, mais de la fréquence des exceptions. Cependant, la dégrédation lorsque la fréquence des exceptions augmente est beaucoup plus dramatique. À seulement une fréquence d'erreur de 25%, le code s'exécutait 24 fois plus lentement. À une fréquence d'erreur de 100%, la version d'exception est presque 100 fois plus lente.

Cela me suggère que peut-être font les mauvais compromis dans nos implémentations d'exception. Les exceptions pourraient être plus rapides, soit en évitant des trajets coûteux, soit en les transformant carrément en vérification de la valeur de retour prise en charge par le compilateur. Jusqu'à ce qu'ils le fassent, nous sommes bloqués en les évitant lorsque nous voulons que notre code s'exécute rapidement.

David Jeske
la source
3

HotSpot est tout à fait capable de supprimer le code d'exception pour les exceptions générées par le système, tant qu'il est tout en ligne. Cependant, les exceptions créées explicitement et celles qui ne sont pas supprimées autrement passent beaucoup de temps à créer la trace de la pile. Remplacez fillInStackTracepour voir comment cela peut affecter les performances.

Tom Hawtin - sellerie
la source
2

Même si lever une exception n'est pas lent, c'est toujours une mauvaise idée de lever des exceptions pour un flux de programme normal. Utilisé de cette façon, il est analogue à un GOTO ...

Je suppose que cela ne répond pas vraiment à la question. J'imagine que la sagesse «conventionnelle» de lancer des exceptions lentement était vraie dans les versions java antérieures (<1.4). La création d'une exception nécessite que la machine virtuelle crée la trace de pile complète. Beaucoup de choses ont changé depuis lors dans la machine virtuelle pour accélérer les choses et c'est probablement un domaine qui a été amélioré.

user38051
la source
1
Il serait bon de définir "flux de programme normal". Beaucoup a été écrit sur l'utilisation des exceptions vérifiées en tant qu'échec du processus métier et une exception non contrôlée pour les échecs non récupérables, donc dans un sens, une défaillance de la logique métier pourrait toujours être considérée comme un flux normal.
Spencer Kormos
2
@Spencer K: Une exception, comme son nom l'indique, signifie qu'une situation exceptionnelle a été découverte (un fichier a disparu, un réseau soudainement fermé, ...). Cela implique que la situation était INATTENDUE. Si l'on s'attend à ce que la situation se produise, je n'utiliserais pas d'exception pour cela.
Mecki
2
@Mecki: à droite. J'ai récemment eu une discussion avec quelqu'un à ce sujet ... Ils écrivaient un cadre de validation et lançaient une exception en cas d'échec de la validation. Je pense que c'est une mauvaise idée car ce serait assez courant. Je préfère voir la méthode renvoyer un ValidationResult.
user38051
2
En termes de flux de contrôle, une exception est analogue à un breakou returnpas un goto.
Hot Licks
3
Il y a des tonnes de paradigmes de programmation. Il ne peut pas y avoir un seul «flux normal», quoi que vous entendiez par là. Fondamentalement, le mécanisme d'exception n'est qu'un moyen de quitter rapidement la trame actuelle et de dérouler la pile jusqu'à un certain point. Le mot «exception» n'implique rien de sa nature «inattendue». Un exemple rapide: il est très naturel de «jeter» des 404 à partir d'applications Web lorsque certaines circonstances se produisent le long du chemin de routage. Pourquoi cette logique ne serait-elle pas mise en œuvre avec des exceptions? Quel est l'anti-modèle?
incarné
2

Comparez simplement, disons Integer.parseInt à la méthode suivante, qui retourne simplement une valeur par défaut dans le cas de données non analysables au lieu de lever une exception:

  public static int parseUnsignedInt(String s, int defaultValue) {
    final int strLength = s.length();
    if (strLength == 0)
      return defaultValue;
    int value = 0;
    for (int i=strLength-1; i>=0; i--) {
      int c = s.charAt(i);
      if (c > 47 && c < 58) {
        c -= 48;
        for (int j=strLength-i; j!=1; j--)
          c *= 10;
        value += c;
      } else {
        return defaultValue;
      }
    }
    return value < 0 ? /* übergebener wert > Integer.MAX_VALUE? */ defaultValue : value;
  }

Tant que vous appliquez les deux méthodes à des données "valides", elles fonctionneront toutes les deux à peu près au même rythme (même si Integer.parseInt parvient à gérer des données plus complexes). Mais dès que vous essayez d'analyser des données invalides (par exemple pour analyser "abc" 1.000.000 fois), la différence de performances devrait être essentielle.

inflamer
la source
2

Un excellent article sur les performances des exceptions est:

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

Instanciation vs réutilisation existante, avec trace de pile et sans, etc:

Benchmark                            Mode   Samples         Mean   Mean error  Units

dynamicException                     avgt        25     1901.196       14.572  ns/op
dynamicException_NoStack             avgt        25       67.029        0.212  ns/op
dynamicException_NoStack_UsedData    avgt        25       68.952        0.441  ns/op
dynamicException_NoStack_UsedStack   avgt        25      137.329        1.039  ns/op
dynamicException_UsedData            avgt        25     1900.770        9.359  ns/op
dynamicException_UsedStack           avgt        25    20033.658      118.600  ns/op

plain                                avgt        25        1.259        0.002  ns/op
staticException                      avgt        25        1.510        0.001  ns/op
staticException_NoStack              avgt        25        1.514        0.003  ns/op
staticException_NoStack_UsedData     avgt        25        4.185        0.015  ns/op
staticException_NoStack_UsedStack    avgt        25       19.110        0.051  ns/op
staticException_UsedData             avgt        25        4.159        0.007  ns/op
staticException_UsedStack            avgt        25       25.144        0.186  ns/op

Selon la profondeur de la trace de la pile:

Benchmark        Mode   Samples         Mean   Mean error  Units

exception_0000   avgt        25     1959.068       30.783  ns/op
exception_0001   avgt        25     1945.958       12.104  ns/op
exception_0002   avgt        25     2063.575       47.708  ns/op
exception_0004   avgt        25     2211.882       29.417  ns/op
exception_0008   avgt        25     2472.729       57.336  ns/op
exception_0016   avgt        25     2950.847       29.863  ns/op
exception_0032   avgt        25     4416.548       50.340  ns/op
exception_0064   avgt        25     6845.140       40.114  ns/op
exception_0128   avgt        25    11774.758       54.299  ns/op
exception_0256   avgt        25    21617.526      101.379  ns/op
exception_0512   avgt        25    42780.434      144.594  ns/op
exception_1024   avgt        25    82839.358      291.434  ns/op

Pour d'autres détails (y compris l'assembleur x64 de JIT), lisez l'article de blog original.

Cela signifie que Hibernate / Spring / etc-EE-shit sont lents en raison des exceptions (xD) et de la réécriture du flux de contrôle des applications loin des exceptions (remplacez-le par continure/ breaket retournez des booleanindicateurs comme en C à partir de l'appel de méthode) améliorez les performances de votre application 10x-100x , selon la fréquence à laquelle vous les lancez))

gavenkoa
la source
0

J'ai changé la réponse de @Mecki ci-dessus pour que method1 retourne un booléen et une vérification dans la méthode d'appel, car vous ne pouvez pas simplement remplacer une exception par rien. Après deux exécutions, la méthode1 était toujours la plus rapide ou aussi rapide que la méthode2.

Voici un aperçu du code:

// Calculates without exception
public boolean method1(int i) {
    value = ((value + i) / i) << 1;
    // Will never be true
    return ((i & 0xFFFFFFF) == 1000000000);

}
....
   for (i = 1; i < 100000000; i++) {
            if (t.method1(i)) {
                System.out.println("Will never be true!");
            }
    }

et résultats:

Exécuter 1

method1 took 841 ms, result was 2
method2 took 841 ms, result was 2
method3 took 85058 ms, result was 2

Exécuter 2

method1 took 821 ms, result was 2
method2 took 838 ms, result was 2
method3 took 85929 ms, result was 2
inder
la source
0

Une exception est destinée à gérer des conditions inattendues au moment de l'exécution uniquement.

L'utilisation d'une exception à la place d'une simple validation qui peut être effectuée au moment de la compilation retardera la validation jusqu'au moment de l'exécution. Cela réduira à son tour l'efficacité du programme.

Lancer une exception au lieu d'utiliser une simple validation if..else rendra également le code complexe à écrire et à maintenir.

Gopinath
la source
-3

Mon avis sur la vitesse d'exception par rapport à la vérification des données par programme.

De nombreuses classes avaient un convertisseur de chaîne en valeur (scanner / analyseur), des bibliothèques respectées et bien connues;)

a généralement une forme

class Example {
public static Example Parse(String input) throws AnyRuntimeParsigException
...
}

le nom d'exception n'est qu'un exemple, n'est généralement pas coché (runtime), donc la déclaration throws n'est que ma photo

existent parfois une deuxième forme:

public static Example Parse(String input, Example defaultValue)

ne jamais jeter

Lorsque le second n'est pas disponible (ou que le programmeur lit trop de documents et n'utilise que le premier), écrivez ce code avec une expression régulière. Les expressions régulières sont cool, politiquement correctes etc:

Xxxxx.regex(".....pattern", src);
if(ImTotallySure)
{
  Example v = Example.Parse(src);
}

avec ce code les programmeurs n'ont pas coûté d'exceptions. MAIS A TOUJOURS un coût comparable très élevé d'expressions régulières par rapport à un petit coût d'exception parfois.

J'utilise presque toujours dans un tel contexte

try { parse } catch(ParsingException ) // concrete exception from javadoc
{
}

sans analyser stacktrace etc, je crois qu'après des cours de vôtre assez rapides.

N'ayez pas peur Exceptions

Jacek Cz
la source
-5

Pourquoi les exceptions devraient-elles être plus lentes que les retours normaux?

Tant que vous n'imprimez pas le stacktrace sur le terminal, l'enregistrez dans un fichier ou quelque chose de similaire, le catch-block ne fait pas plus de travail que les autres blocs de code. Donc, je ne peux pas imaginer pourquoi "throw new my_cool_error ()" devrait être si lent.

Bonne question et j'ai hâte d'avoir plus d'informations sur ce sujet!

qualbeen
la source
17
L'exception doit capturer les informations sur la trace de la pile, même si elle n'est pas réellement utilisée.
Jon Skeet