Quelle est la lenteur des exceptions Java?

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

La sagesse conventionnelle, ainsi que beaucoup de résultats Google, indiquent qu’une logique exceptionnelle ne devrait pas être utilisée pour un stream 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 concerne le n ° 1.

Par exemple, cette page décrit la gestion des exceptions Java comme étant “très lente” et associe la lenteur à la création de la chaîne de message d’exception – “cette chaîne est ensuite utilisée pour créer l’object exception renvoyé. Ce n’est pas rapide.” L’article Gestion efficace des exceptions en Java indique que “la raison en est due à l’aspect de création d’object de la gestion des exceptions, qui rend ainsi les exceptions de lancement insortingnsèquement lentes”. Une autre raison est que la génération de trace de la stack est ce qui la ralentit.

Mes tests (utilisant Java 1.6.0_07, Java HotSpot 10.0, sous 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. A 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é de faire fonctionner les méthodes dans des ordres différents et de faire la moyenne de mes temps de test, pensant que cela pouvait être dû à l’échauffement de la JVM. Dans tous mes tests, le lancer était au moins aussi rapide que le retour, sinon plus rapide (jusqu’à 3,1% plus rapide). Je suis tout à fait ouvert à la possibilité que mes tests soient incorrects, mais je n’ai rien vu d’échantillon de code, de comparaisons de tests ou de résultats au cours des deux dernières années montrant que la gestion des exceptions était réellement lent.

Ce qui m’a conduit dans cette voie, c’était une API que je devais utiliser pour lancer des exceptions dans le cadre d’une logique de contrôle normale. Je voulais les corriger dans leur utilisation, mais maintenant je ne peux pas le faire. Devrai-je plutôt les féliciter pour leur reflection prospective?

Dans le document Gestion 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. . Je n’ai pas encore testé cette théorie.

Cela dépend de la manière dont les exceptions sont implémentées. Le moyen le plus simple consiste à utiliser setjmp et longjmp. Cela signifie que tous les registres du CPU sont écrits dans la stack (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 stack et restaurer les valeurs de tous les registres (ainsi que d’autres valeurs possibles dans la machine virtuelle). Donc essayer et lancer sont tout aussi lents, et c’est assez lent, mais si aucune exception n’est levée, quitter le bloc try ne prend aucun temps dans la plupart des cas (car tout est mis sur la stack qui se nettoie automatiquement si la méthode existe).

Sun et les 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 un autre moyen d’implémenter les exceptions, ce qui accélère l’essayage (en fait, rien ne se passe pour l’essentiel – tout ce qui doit se passer est déjà fait lorsque la VM est chargée) et le lancement est moins lent. . Je ne sais pas quelle JVM utilise cette nouvelle et meilleure technique …

… mais écrivez-vous en Java pour que votre code ne s’exécute plus que sur une JVM sur un système spécifique? Étant donné que s’il peut être exécuté sur une autre plate-forme ou une autre version de JVM (éventuellement d’un autre fournisseur), qui dit utiliser également cette implémentation rapide? Le rapide est plus compliqué que le lent et pas facilement possible sur tous les systèmes. Vous voulez restr portable? Alors ne comptez pas sur les exceptions pour être rapide.

Cela fait aussi une grande différence dans ce que vous faites dans un bloc try. Si vous ouvrez un bloc d’essai et n’appelez jamais de méthode depuis ce bloc d’essai, le bloc d’essai sera ultra rapide, car le JIT peut alors traiter un jet comme un simple passage. Il n’a pas non plus besoin de sauvegarder l’état de la stack ni de dérouler la stack si une exception est levée (il suffit de passer directement aux gestionnaires de capture). Cependant, ce n’est pas ce que vous faites habituellement. Habituellement, vous ouvrez un bloc try puis appelez une méthode qui pourrait déclencher une exception, n’est-ce pas? Et même si vous utilisez simplement le bloc try dans votre méthode, quelle sorte de méthode cela fera-t-il, qui n’appelle aucune autre méthode? Va-t-il simplement calculer un nombre? Alors pour quoi avez-vous besoin d’exceptions? Il existe des moyens beaucoup plus élégants pour réguler les stream de programmes. Pour à peu près tout 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 par rapport au bloc try est trop faible pour exclure des facteurs de confusion tels que les processus d'arrière-plan. Mais le bloc de capture a tout tué et l'a rendu 66 fois plus lent!

Comme je l'ai dit, le résultat ne sera pas si mauvais si vous mettez try / catch et jetez tout dans la même méthode (method3), mais il ne s'agit pas d'une optimisation JIT spéciale. Et même en utilisant cette optimisation, le lancer est encore assez lent. Donc, je ne sais 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.

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 trois premiers sont les mêmes que ceux de Mecki (mon ordinateur portable est évidemment plus lent).

method4 est identique à method3 sauf qu’il crée un new Integer(1) plutôt que de throw new Exception() .

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

method6 est comme method3, sauf qu’il génère une exception pré-créée (une variable d’instance) au lieu d’en créer une nouvelle.

En Java, une grande partie du coût de la création d’une exception est le temps passé à rassembler la trace de la stack, ce qui se produit lorsque l’object d’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 création de l’exception.

Aleksey Shipilëv a fait une parsing très approfondie dans laquelle il compare les exceptions Java sous différentes combinaisons de conditions:

  • Exceptions nouvellement créées vs exceptions pré-créées
  • Trace de stack activée vs désactivée
  • Trace de trace demandée vs jamais demandée
  • Pris au plus haut niveau et relancé à tous les niveaux vs enchaîné / emballé à tous les niveaux
  • Différents niveaux de profondeur de la stack d’appels Java
  • Pas d’optimisation par rapport aux parameters d’inclusion par rapport aux parameters par défaut
  • Champs définis par l’utilisateur lus vs 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 textuellement de son poste) étaient les suivantes:

  1. Les exceptions vraiment exceptionnelles sont magnifiquement performantes. Si vous les utilisez comme prévu et que vous ne communiquez que les cas vraiment exceptionnels parmi le très grand nombre de cas non exceptionnels traités par du code normal, l’utilisation des exceptions est la solution gagnante.

  2. Les coûts de performance des exceptions ont deux composants principaux: la construction de trace de la stack lorsque Exception est instanciée et le déroulement de la stack lors du lancement de l’exception.

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

  4. Les coûts de déroulement de la stack dépendent de la chance que nous avons de rapprocher le gestionnaire des exceptions dans le code compilé. Structurer soigneusement le code pour éviter la recherche de gestionnaires d’exceptions en profondeur 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 twig locale. Quelle que soit sa beauté, cela ne signifie pas que vous devez utiliser les exceptions comme stream 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 éventuel de lever l’exception réelle.

  6. La règle de l’optimisme optimiste semble être une fréquence de 10 ^ -4 pour les exceptions suffisamment exceptionnelle. Cela, bien sûr, dépend des poids lourds des exceptions elles-mêmes, des actions exactes sockets dans les gestionnaires d’exceptions, etc.

Le résultat est que quand une exception n’est pas lancée, vous ne payez pas de coût, alors lorsque la condition exceptionnelle est suffisamment rare, la gestion des exceptions est plus rapide que l’utilisation d’un if chaque fois. Le post complet vaut bien une lecture.

Ma réponse, malheureusement, est trop longue pour poster ici. Alors laissez-moi résumer ici et 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 en tant qu’exceptions” par rapport au “code qui n’échoue jamais”? ” comme la réponse acceptée pourrait vous faire croire. Au lieu de cela, la question devrait être “Quelle est la lenteur des échecs signalés comme des exceptions” par rapport aux échecs signalés d’autres manières? ” En règle générale, les deux autres manières de signaler les défaillances sont les valeurs sentinelles ou les enveloppes de résultat.

Les valeurs Sentinel tentent de renvoyer une classe en cas de succès et une autre en cas d’échec. Vous pouvez penser à cela presque comme renvoyant une exception au lieu d’en lancer une. Cela nécessite une classe parente partagée avec l’object de réussite, puis une vérification “instanceof” et un couple pour obtenir les informations de réussite ou d’échec.

Au risque de la sécurité de type, il s’avère que les valeurs de Sentinel sont plus rapides que les exceptions, mais seulement d’un facteur 2 environ. Cela peut sembler beaucoup, mais cela ne couvre que le coût de la différence d’implémentation. En pratique, le facteur est beaucoup plus bas puisque nos méthodes qui pourraient échouer sont beaucoup plus intéressantes que quelques opérateurs arithmétiques comme dans l’exemple de code ailleurs sur cette page.

Résultat En revanche, les enveloppes ne sacrifient pas la sécurité du type. Ils enveloppent 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 objects succès et échec. Cependant, les objects de résultat sont environ deux fois plus lents que ceux utilisant des exceptions. Il s’avère que créer un nouvel object wrapper à chaque fois coûte beaucoup plus cher que de lancer une exception parfois.

De plus, des exceptions sont le langage fourni pour indiquer qu’une méthode peut échouer. Il n’existe pas d’autre moyen de dire, à partir de l’API, que les méthodes sont censées toujours (principalement) fonctionner et qu’elles sont censées rapporter des erreurs.

Les exceptions sont plus sûres que les sentinelles, plus rapides que les objects de résultat et moins surprenantes que les autres. Je ne suggère pas que try / catch remplace if / else, mais les exceptions sont le bon moyen de signaler une défaillance, même dans la logique métier.

Cela dit, je voudrais souligner que les deux moyens les plus fréquents d’impact sur les performances que j’ai connus sont la création d’objects inutiles et de boucles nestedes. Si vous avez le choix entre créer une exception ou ne pas créer une exception, ne créez pas l’exception. Si vous avez le choix entre créer parfois une exception ou créer un autre object à tout moment, créez l’exception.

J’ai étendu les réponses données par @Mecki et @incarnate , sans remplissage de la stack pour Java.

Avec Java 7+, nous pouvons utiliser Throwable(Ssortingng message, Throwable cause, boolean enableSuppression,boolean writableStackTrace) pouvant être Throwable(Ssortingng 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 

Donc, les méthodes qui retournent des valeurs sont plus rapides, comparées aux méthodes générant des exceptions. À mon humble avis, nous ne pouvons pas concevoir une API claire en utilisant uniquement les types de retour pour les stream de succès 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); } } 

Je ne sais pas si ces sujets sont liés, mais je voulais une fois implémenter une astuce basée sur la trace de la stack du thread en cours: je voulais découvrir le nom de la méthode, qui a déclenché l’instanciation dans la classe instanciée (yeap, l’idée est folle, Je l’ai totalement abandonné). J’ai donc découvert que l’appel de Thread.currentThread().getStackTrace() est extrêmement lent (en raison de la méthode native dumpThreads qu’il utilise en interne).

Ainsi, Java Throwable possède une méthode native fillInStackTrace . Je pense que le bloc Killer- catch décrit précédemment 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 fonctionnelles sont compilées dans JVM à l’aide de ControlThrowable , qui étend Throwable et remplace ses fillInStackTrace de 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) } 

Donc, les résultats sont les suivants:

 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 la method3 et la method4 est qu’elles method4 différents types d’exceptions. Yeap, method4 est encore plus lent que method1 et method2 , mais la différence est beaucoup plus acceptable.

Je pense que le premier article fait référence au fait de traverser la stack d’appels et de créer une trace de stack comme étant la partie la plus coûteuse, alors que le deuxième article ne le dit pas. 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 stack, etc.)

Mais encore – je pense que cela ne devrait être considéré que comme un mal nécessaire, un dernier recours. La raison de John pour cela est d’émuler des fonctionnalités dans d’autres langages qui ne sont pas (encore) disponibles dans la JVM. Vous ne devez PAS prendre l’habitude d’utiliser des exceptions pour le stream de contrôle. Surtout pas pour des raisons de performance! Comme vous le mentionnez vous-même dans # 2, vous risquez de masquer des bogues graves dans votre code, et cela sera plus difficile à gérer pour les nouveaux programmeurs.

Les microbenchmarks en Java sont étonnamment difficiles à obtenir (on me l’a dit), en particulier lorsque l’on aborde le territoire JIT, alors 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 quelque part entre 2 et 5 frames de stack dans votre test? Imaginez maintenant que votre code sera appelé par un composant JSF déployé par JBoss. Vous pouvez maintenant avoir une trace de stack de plusieurs pages.

Vous pourriez peut-être poster votre code de test?

J’ai effectué des tests de performances avec JVM 1.5 et l’utilisation des exceptions était au moins deux fois plus lente. En moyenne: le temps d’exécution sur une méthode sortingvialement petite a plus que sortingplé (3 fois) avec des exceptions. Une petite boucle sortingviale qui devait attraper l’exception a vu une augmentation de 2 fois de son temps personnel.

J’ai vu des chiffres similaires dans le code de production ainsi que des micro-benchmarks.

Les exceptions ne doivent absolument 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)

Utiliser une exception pour signaler une mauvaise valeur sur un formulaire d’interface utilisateur, probablement pas si mal du sharepoint vue des performances.

Que ce soit ou non une bonne pratique de conception, j’irais avec la règle: si l’erreur est normale / attendue, alors utilisez une valeur de retour. Si c’est anormal, utilisez une exception. Par exemple: la lecture des entrées utilisateur, les mauvaises valeurs sont normales – utilisez un code d’erreur. Passer une valeur à une fonction d’utilitaire interne, filtrer les mauvaises valeurs en appelant le code – utiliser une exception.

Il y a quelque temps, j’ai écrit une classe pour tester la performance relative de la conversion des chaînes en ints 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é le regex de la manière la plus efficace possible (c.-à-d. Créer les objects Pattern et Matcher avant d’interfacer la boucle), et je n’ai pas imprimé ou enregistré les emstackments à partir des exceptions.

Pour une liste de dix mille chaînes, si elles étaient toutes des nombres valides, l’approche parseInt () était quatre fois plus rapide que l’approche regex. Mais si seulement 80% des chaînes étaient valides, le regex était deux fois plus rapide que parseInt (). Et si 20% étaient valides, signifiant que l’exception avait été lancée et prise 80% du temps, le regex était environ vingt fois plus rapide que parseInt ().

J’ai été surpris par le résultat, considérant que l’approche regex traite deux fois les chaînes valides: une fois pour la partie et à nouveau pour parseInt (). Mais jeter 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 des exceptions. Mais si vous ne faites que valider une entrée utilisateur ou quelque chose du genre, utilisez bien l’approche parseInt ().

Même si lancer une exception n’est pas lent, il est toujours une mauvaise idée de lancer des exceptions pour un stream 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 la levée des exceptions était vraie dans les versions précédentes de Java (<1.4). La création d'une exception nécessite que la machine virtuelle crée l'intégralité de la trace de la pile. Beaucoup de choses ont changé depuis lors dans la VM pour accélérer les choses et c'est probablement un domaine qui a été amélioré.

HotSpot est tout à fait capable de supprimer le code d’exception pour les exceptions générées par le système, pour autant qu’il soit entièrement intégré. Cependant, les exceptions explicitement créées et celles qui n’ont pas été supprimées passent beaucoup de temps à créer la trace de la stack. Remplacez fillInStackTrace pour voir comment cela peut affecter les performances.

Les performances d’exception en Java et en C # laissent beaucoup à désirer.

En tant que programmeurs, cela nous oblige à vivre selon la règle “les exceptions doivent être occasionnées rarement”, simplement pour des raisons de performances pratiques.

Cependant, en tant qu’informaticiens, nous devrions 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 ou de la probabilité de réussite ou d’échec. Seul l’appelant a cette information. Essayer d’éviter les exceptions conduit à des identifiants d’API peu clairs où, dans certains cas, nous avons uniquement des versions d’exception propres mais lentes, et dans d’autres cas, des erreurs de retour rapides et maladroites. . The library implementor may have to write and maintain two versions of APIs, and the caller has to decide which of two versions to use in each situation.

This is kind of a mess. If exceptions had better performance, we could avoid these clunky idioms and use exceptions as they were meant to be used… as a structured error return facility.

I’d really like to see exception mechanisms implemented using techniques closer to return-values, so we could have performance closer to return values.. since this is what we revert to in performance sensitive code.

Here is a code-sample that compares exception performance to error-return-value performance.

public class 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); } } } 

}

And here are the results:

 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 

Checking and propagating return-values does add some cost vs the baseline-null call, and that cost is proportional to call-depth. At a call-chain depth of 8, the error-return-value checking version was about 27% slower than the basline version which did not check return values.

Exception performance, in comparison, is not a function of call-depth, but of exception frequency. However, the degredation as exception frequency increases is much more dramatic. At only a 25% error frequency, the code ran 24-TIMES slower. At an error frequency of 100%, the exception version is almost 100-TIMES slower.

This suggests to me that perhaps are making the wrong tradeoffs in our exception implementations. Exceptions could be faster, either by avoiding costly stalk-walks, or by ousortingght turning them into comstackr supported return-value checking. Until they do, we're stuck avoiding them when we want our code to run fast.

Just compare let’s say Integer.parseInt to the following method, which just returns a default value in the case of unparseable data instead of throwing an Exception:

  public static int parseUnsignedInt(Ssortingng 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; } 

As long as you apply both methods to “valid” data, they both will work at approximately the same rate (even although Integer.parseInt manages to handle more complex data). But as soon as you try to parse invalid data (eg to parse “abc” 1.000.000 times), the difference in performance should be essential.

I changed @Mecki ‘s answer above to have method1 return a boolean and a check in the calling method, as you cannot just replace an Exception with nothing. After two runs, method1 was still either the fastest or as fast as method2.

Here is snapshot of the 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!"); } } 

and results:

Run 1

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

Courir 2

 method1 took 821 ms, result was 2 method2 took 838 ms, result was 2 method3 took 85929 ms, result was 2 

Great post about exception performance is:

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

Instantiating vs reusing existing, with stack trace and without, 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 

Depending on depth of stack trace:

 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 

For other details (including x64 assembler from JIT) read original blog post.

That mean Hibernate/Spring/etc-EE-shit are slow because of exceptions (xD) and rewriting app control flow away from exceptions (replace it with continure / break and returning boolean flags like in C from method call) improve performance of your application 10x-100x, depending on how often you throws them ))

My opinion about Exception speed versus checking data programmatically.

Many classes had Ssortingng to value converter (scanner / parser), respected and well-known libraries too 😉

usually has form

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

exception name is only example, usually is unchecked (runtime), so throws declaration is only my picture

sometimes exist second form:

 public static Example Parse(Ssortingng input, Example defaultValue) 

never throwing

When the second ins’t available (or programmer read too less docs and use only first), write such code with regular expression. Regular expression are cool, politically correct etc:

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

with this code programmers hasn’t cost of exceptions. BUT HAS comparable very HIGH cost of regular expressions ALWAYS versus small cost of exception sometimes.

I use almost always in such context

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

without analysing stacktrace etc, I believe after lectures of Yours quite speed.

Do not be afraid Exceptions

Why should exceptions be any slower than normal returns?

As long as you don’t print the stacktrace to the terminal, save it into a file or something similar, the catch-block doesn’t do any more work than other code-blocks. So, I can’t imagine why “throw new my_cool_error()” should be that slow.

Good question and I’m looking forward to further information on this topic!