542 votes

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

Question : Le traitement des exceptions en Java est-il réellement lent ?

Selon la sagesse conventionnelle, ainsi que de nombreux résultats de Google, la logique exceptionnelle ne devrait pas être utilisée pour le déroulement normal d'un programme en Java. Deux raisons sont généralement avancées,

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

y

  1. il est désordonné parce que les gens s'attendent à ce que seules les erreurs soient traitées dans du code exceptionnel.

Cette question porte sur le numéro 1.

A titre d'exemple, cette page décrit la gestion des exceptions Java comme étant "très lente" et établit un lien entre cette lenteur et la création de la chaîne de messages d'exception - "cette chaîne est ensuite utilisée pour créer l'objet d'exception qui est lancé. Ce n'est pas rapide". L'article Gestion efficace des exceptions en Java dit que "la raison de ceci est due à l'aspect de création d'objet de la gestion des exceptions, ce qui rend le lancement d'exceptions intrinsèquement lent". Une autre raison est que la génération de la trace de la pile est ce qui ralentit le processus.

Mes tests (avec 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 o lancez . 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 calculer la moyenne des temps de test, pensant que cela pouvait être dû au réchauffement de la JVM. Dans tous mes tests, l'envoi était au moins aussi rapide que le retour, si ce n'est plus (jusqu'à 3,1 % plus rapide). Je suis tout à fait ouvert à la possibilité que mes tests soient erronés, mais je n'ai rien vu dans les échantillons de code, les comparaisons de tests ou les résultats depuis un an ou deux qui montre que la gestion des exceptions en Java est réellement lente.

Ce qui m'a conduit sur 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 je ne pourrai peut-être pas le faire. Devrai-je plutôt les féliciter pour leur esprit d'initiative ?

Dans le document Traitement efficace des exceptions Java dans la compilation juste-à-temps Les auteurs suggèrent que la seule présence de gestionnaires d'exceptions, même si aucune exception n'est levée, est suffisante pour empêcher le compilateur JIT d'optimiser correctement le code, ce qui le ralentit. Je n'ai pas encore testé cette théorie.

12 votes

Je sais que votre question ne concernait pas le point 2), mais vous devriez vraiment reconnaître que l'utilisation d'une exception pour le déroulement du programme n'est pas mieux que l'utilisation de GOTOs. Certaines personnes défendent les gotos, d'autres défendent ce dont vous parlez, mais si vous demandez à quelqu'un qui a implémenté et maintenu l'un ou l'autre pendant un certain temps, il vous dira que les deux sont des pratiques de conception pauvres et difficiles à maintenir (et il maudira probablement le nom de la personne qui a pensé être assez intelligente pour prendre la décision de les utiliser).

92 votes

Bill, prétendre que l'utilisation d'exceptions pour le déroulement du programme n'est pas meilleure que l'utilisation de GOTO n'est pas mieux que de prétendre que l'utilisation de conditionnels et de boucles pour le déroulement du programme n'est pas meilleure que l'utilisation de GOTO. C'est un faux-fuyant. Expliquez-vous. Les exceptions peuvent et sont utilisées efficacement pour le déroulement du programme dans d'autres langages. Le code Python idiomatique utilise régulièrement les exceptions, par exemple. Je peux et j'ai maintenu du code qui utilise les exceptions de cette manière (pas en Java cependant), et je ne pense pas qu'il y ait quelque chose d'intrinsèquement mauvais à cela.

5 votes

Notez que certains frameworks web utilisent les exceptions comme un moyen pratique de rediriger les données, par exemple l'interface de Wicket. RestartResponseException . Cela n'arrive que quelques fois par demande, généralement pas, et je peux difficilement imaginer un moyen plus pratique dans un cadre de composants orienté Java.

368voto

Mecki Points 35351

Cela dépend de la manière dont les exceptions sont mises en œuvre. La façon la plus simple est d'utiliser setjmp et longjmp. Cela signifie que tous les registres du CPU sont écrits sur la pile (ce qui prend déjà un certain temps) et que d'autres données doivent éventuellement ê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 VM). Ainsi, try et throw sont aussi lents l'un que l'autre, ce qui est plutôt lent, mais si aucune exception n'est levée, la sortie du bloc try ne prend aucun temps dans la plupart des cas (puisque tout est placé sur la pile qui se nettoie automatiquement si la méthode existe).

Sun et d'autres ont reconnu que cela pouvait ê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 les exceptions, qui rend le try lui-même rapide comme l'éclair (en fait, il ne se passe rien du tout pour le try en général - tout ce qui doit se passer est déjà fait lorsque la classe est chargée par la VM) et qui rend le throw pas aussi lent. Je ne sais pas quelle JVM utilise cette nouvelle et meilleure technique...

...mais est-ce que vous écrivez en Java pour que votre code ne s'exécute plus tard que sur une JVM et un système spécifique ? Puisque s'il est susceptible de fonctionner sur une autre plate-forme ou une autre version de JVM (éventuellement d'un autre fournisseur), qui dit qu'il utilise également l'implémentation rapide ? L'implémentation rapide est plus compliquée que l'implémentation lente et n'est pas facilement réalisable sur tous les systèmes. Vous voulez rester portable ? Alors ne comptez pas sur la rapidité des exceptions.

Ce que vous faites à l'intérieur d'un bloc d'essai fait également une grande différence. Si vous ouvrez un bloc d'essai et n'appelez jamais aucune méthode à l'intérieur de ce bloc d'essai, celui-ci sera ultra rapide, car le JIT peut alors traiter un throw comme un simple goto. Il n'a pas besoin de sauvegarder l'état de la pile ni de la dérouler si une exception est levée (il doit seulement sauter vers les gestionnaires de capture). Cependant, ce n'est pas ce que vous faites habituellement. Habituellement, vous ouvrez un bloc d'essai et vous appelez ensuite une méthode qui pourrait lever une exception, n'est-ce pas ? Et même si vous n'utilisez que le bloc d'essai dans votre méthode, quel genre de méthode sera-t-elle, qui n'appelle aucune autre méthode ? Est-ce qu'elle va juste calculer un nombre ? Alors pourquoi avez-vous besoin d'exceptions ? Il existe des moyens bien plus élégants de réguler le flux du programme. Pour à peu près tout le reste, sauf les mathématiques simples, vous devrez appeler une méthode externe et cela détruit déjà l'avantage d'un bloc try local.

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 dû au bloc d'essai est trop faible pour exclure des facteurs confondants tels que les processus d'arrière-plan. Mais le bloc catch tue tout et rend le processus 66 fois plus lent !

Comme je l'ai dit, le résultat ne sera pas si mauvais si vous mettez try/catch et throw dans la même méthode (method3), mais c'est une optimisation JIT spéciale sur laquelle je ne compterais pas. Et même en utilisant cette optimisation, le throw est toujours 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.

0 votes

Excellente question et réponse, merci John et Mecki ! J'ai hérité d'un code qui applique le try-catch à n'importe quelle méthode (y compris les setter/getter qui renvoient simplement la valeur d'une propriété) et je cherchais à en savoir plus sur les implications en termes de performances. La question et la réponse m'ont donné tous les détails à ce sujet ;). Bonne chance, stef.

0 votes

@Mecki Comment se fait-il que vos performances soient si affectées alors que JDK Hotspot devrait mettre en cache ces exceptions. javaspecialists.eu/archive/Issue187.html Avez-vous désactivé cette option pour effectuer vos tests ?

0 votes

Ahh après une longue période de débogage, je pense que lorsque le code lance une nouvelle exception via throw new Exception() Le JDK ne le mettra pas en cache. Mais si Java le lance, par exemple ((Object)null).getClass() alors java le mettra en cache.

279voto

Hot Licks Points 25075

Pour info, j'ai étendu l'expérience que Mecki a faite :

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éthode4 est identique à la méthode3, sauf qu'elle crée un fichier new Integer(1) plutôt que de faire throw new Exception() .

La méthode5 est semblable à la méthode3, sauf qu'elle crée le fichier new Exception() sans le jeter.

La méthode6 ressemble à la méthode3, sauf qu'elle lève une exception pré-créée (une variable d'instance) au lieu d'en créer une nouvelle.

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

58 votes

+1 Votre réponse aborde le problème principal - le temps pris pour dérouler la pile et la tracer, et accessoirement l'émission de l'erreur. J'aurais choisi cette réponse comme réponse finale.

11 votes

Sympa. ~70% pour créer l'exception, ~30% pour la lancer. Bonne info.

1 votes

Question similaire : combien de frais généraux supplémentaires y a-t-il à attraper une exception, à la relancer et à l'attraper à nouveau ? Merci.

43voto

Fuwjax Points 581

Ma réponse, malheureusement, est trop longue pour être publiée ici. Je vais donc la résumer ici et vous renvoyer à http://www.fuwjax.com/how-slow-are-java-exceptions/ pour les détails croustillants.

La vraie question ici n'est pas "Dans quelle mesure les "échecs signalés comme des exceptions" sont-ils lents par rapport au "code qui n'échoue jamais", comme la réponse acceptée pourrait vous le faire croire. La question devrait plutôt être "Quelle est la lenteur des "échecs signalés comme des exceptions" par rapport aux échecs signalés d'autres manières ?". En général, les deux autres façons de signaler les échecs sont soit avec des valeurs sentinelles, soit avec des enveloppes 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. On peut considérer que c'est comme renvoyer 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 de type "instanceof" et quelques transferts pour obtenir les informations de réussite ou d'échec.

Il s'avère qu'au risque de la sécurité des types, les valeurs Sentinel sont plus rapides que les exceptions, mais seulement par un facteur d'environ 2x. Cela peut sembler beaucoup, mais ce facteur 2x ne couvre que le coût de la différence d'implémentation. En pratique, le facteur est beaucoup plus faible, car les méthodes susceptibles d'échouer sont beaucoup plus intéressantes que quelques opérateurs arithmétiques comme dans l'exemple de code présenté dans cette page.

Les wrappers de résultat, quant à eux, ne sacrifient en rien la sécurité des types. Ils regroupent les informations de succès et d'échec dans une seule classe. Ainsi, au lieu de "instanceof", ils fournissent un "isSuccess()" et des récupérateurs pour les objets de succès et d'échec. Cependant, les objets de résultat sont à peu près 2x plus lent que d'utiliser des exceptions. Il s'avère que créer un nouvel objet wrapper à chaque fois est beaucoup plus coûteux que de lancer une exception de temps en temps.

En outre, les exceptions sont le moyen fourni par le langage pour indiquer qu'une méthode peut échouer. Il n'y a pas d'autre moyen de savoir, à partir de l'API, quelles méthodes sont censées toujours (ou presque) fonctionner et lesquelles sont censées signaler un échec.

Les exceptions sont plus sûres que les sentinelles, plus rapides que les objets de résultat, 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 tiens à souligner que les deux façons les plus fréquentes d'affecter considérablement les performances que j'ai rencontrées sont la création d'objets inutiles et les boucles imbriquées. Si vous avez le choix entre créer une exception ou ne pas en créer, ne créez pas d'exception. Si vous avez le choix entre créer une exception parfois ou créer un autre objet tout le temps, alors créez l'exception.

3 votes

Juste pour le plaisir, j'ai désactivé fillInStackTrace dans le test des exceptions. Voici les temps maintenant : Contrôle 347 Exception 351 Résultat 364 Sentinelle 355

1 votes

Fuwjax, à moins que quelque chose ne m'échappe (et j'admets que je n'ai lu que votre article sur le SO, pas celui de votre blog), il semble que vos deux commentaires ci-dessus contredisent votre article. Je présume que les chiffres les plus bas sont les meilleurs dans votre benchmark, n'est-ce pas ? 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 faibles que les deux autres techniques que vous décrivez. Est-ce que j'ai raté quelque chose, ou est-ce que vous avez fait un commentaire pour réfuter votre post ?

0 votes

@Fuwjax - la façon d'éviter le choix "entre le marteau et l'enclume" que vous présentez ici, est de Pré-affecter un objet qui représente le "succès". En général, on peut aussi pré-allouer des objets pour les cas d'échec courants. Ce n'est que dans les rares cas où il faut renvoyer des détails supplémentaires qu'un nouvel objet est créé. (C'est 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).

8voto

Lars Westergren Points 1362

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 la plus coûteuse, et bien que le second 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 dans lequel 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 quand même, je pense que cela ne doit ê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 langages qui ne sont pas (encore) disponibles dans la JVM. Vous ne devriez 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 point 2, vous risquez de masquer de sérieux bogues dans votre code de cette façon, et il sera plus difficile à maintenir pour les nouveaux programmeurs.

Les microbenchmarks en Java sont étonnamment difficiles à réaliser (c'est ce qu'on m'a dit), surtout quand on entre dans le domaine du JIT, donc je doute vraiment que l'utilisation des exceptions soit plus rapide que le "retour" dans la vie réelle. Par exemple, je soupçonne que vous avez entre 2 et 5 stack frames dans votre test ? Imaginez maintenant que votre code soit invoqué par un composant JSF déployé par JBoss. Vous pourriez avoir une trace de pile de plusieurs pages.

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

7voto

Will Points 30630

J'ai travaillé sur certains des premiers midlets java pour les premiers téléphones mobiles compatibles java.

Nous avons rapidement appris à écrire le java "style c".

Cependant, une chose qui me reste en mémoire est l'itération sur chaque élément d'un vecteur - il était beaucoup plus rapide de ne pas vérifier les limites et d'attraper une exception de type array-index-out-of-bounds que de vérifier les limites dans le code de la boucle !

Juste un point de données pour vous.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X