Question: La gestion des exceptions dans Java est-elle réellement lente?
La sagesse conventionnelle, ainsi que de nombreux résultats Google, indique qu'une logique exceptionnelle ne devrait pas être utilisée pour le déroulement normal des programmes en Java. Deux raisons sont généralement données,
et
Cette question concerne le n ° 1.
Par exemple, cette page décrit Java le traitement des exceptions comme "très lent" et associe la lenteur à la création de la chaîne du message d'exception - "cette chaîne est ensuite utilisée pour la objet exception qui est levé. Ce n'est pas rapide. " L'article Gestion effective des exceptions en Java indique que "la raison en est due à l'aspect 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 traces de pile est ce qui la ralentit.
Mes tests (avec 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 renvoyer ou jeter . De cette façon, le traitement réel est le même. J'ai essayé d'exécuter les méthodes dans différents ordres et de faire la moyenne de mes temps de test, en pensant que c'était peut-être le réchauffement de 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 erronés, mais je n'ai rien vu qui existe sous la forme d'un exemple de code, de comparaisons de tests ou de résultats au cours des deux dernières années montrant la gestion des exceptions dans Java être réellement lent.
Ce qui m'a conduit dans cette voie, c'est une API que je devais utiliser et qui renvoyait des exceptions dans le cadre d'une logique de contrôle normale. Je voulais les corriger dans leur utilisation, mais maintenant, je ne pourrais peut-être pas le faire. Devrai-je plutôt les féliciter pour leur réflexion 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'exception suffit, même si aucune exception n'est générée, pour empêcher le compilateur JIT d’optimiser correctement le code, le ralentissant ainsi. 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 de la CPU sont écrits dans la pile (ce qui prend déjà un peu de temps) et que d'autres données doivent éventuellement être créées ... tout cela se passe déjà dans l'instruction try. L'instruction throw doit dérouler la pile et restaurer les valeurs de tous les registres (et d'autres valeurs possibles dans la VM). Donc essayer et lancer sont également lents, et c'est assez lent, cependant si aucune exception n'est levée, la sortie du bloc try ne prend pas de temps dans la plupart des cas (comme tout est mis sur la pile qui nettoie automatiquement si la méthode existe).
Sun et d’autres ont reconnu que c’était peut-être sous-optimal et que, bien entendu, les machines virtuelles allaient de plus en plus vite. Il existe un autre moyen d'implémenter des exceptions, ce qui rend l'essayage rapide (en fait, rien ne se passe pour essayer en général - tout ce qui doit se passer est déjà fait lorsque la classe est chargée par la machine virtuelle) et rend le lancer moins lent. . Je ne sais pas quelle machine virtuelle utilise cette nouvelle et meilleure technique ...
... mais vous écrivez dans Java afin que votre code ne fonctionne plus tard que sur une seule machine virtuelle sur un système spécifique? Etant donné que si elle peut être exécutée sur une autre plate-forme ou une autre version de la JVM (éventuellement de tout autre fournisseur), qui dit utiliser également l'implémentation rapide? Le rapide est plus compliqué que le lent et pas facilement possible sur tous les systèmes. Vous voulez rester portable? Alors ne comptez pas sur les exceptions étant rapides.
Ce que vous faites dans un bloc try fait également une grande différence. Si vous ouvrez un bloc try et n'appelez jamais une méthode à l'intérieur de ce bloc try, le bloc try sera extrêmement rapide, car le JIT peut alors traiter un lancer comme un simple goto. Il n'a pas non plus besoin de sauvegarder l'état de pile ni de dérouler la pile si une exception est levée (il suffit de sauter 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 susceptible de générer une exception, n'est-ce pas? Et même si vous utilisez simplement le bloc try dans votre méthode, de quel type de méthode s'agit-il, cela 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églementer le déroulement du programme. Pour à peu près tout autre chose que de simples mathématiques, vous devrez appeler une méthode externe, ce qui 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 try est trop faible pour éliminer les facteurs de confusion tels que les processus en arrière-plan. Mais le bloc de prise a tout tué et rendu 66 fois plus lent!
Comme je l'ai dit, le résultat ne sera pas si mauvais si vous mettez try/catch and throw au sein de la même méthode (method3), mais il s'agit d'une optimisation spéciale JIT sur laquelle je ne compterais pas. 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 existe certainement un meilleur moyen de le faire que d’essayer d’essayer/attraper/lancer.
Pour votre information, 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).
method4 est identique à method3 à la différence qu'il crée une new Integer(1)
plutôt que de faire throw new Exception()
.
method5 est comme method3 sauf qu'il crée la 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.
Dans Java une grande partie des dépenses liées à la levée d'une exception correspond au temps passé à collecter la trace de la pile, ce qui se produit lors de la création de l'objet exception. 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 effectué une analyse très approfondie dans laquelle il compare Java exceptions sous diverses combinaisons de conditions:
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 dans son message) sont les suivantes:
Les exceptions vraiment exceptionnelles sont merveilleusement performantes. Si vous les utilisez comme prévu et 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 d'exceptions est le gain de performance .
Les coûts de performance des exceptions ont deux composants principaux: construction de la trace de pile quand une exception est instanciée et déroulement de la pile pendant la levée de l'exception.
Les coûts de construction des traces de pile sont proportionnels à la profondeur de la pile au moment de l'instanciation des exceptions. C'est déjà grave, car qui sait sur la Terre la profondeur de la pile à laquelle cette méthode de projection serait appelée? Même si vous désactivez la génération de trace de la pile et/ou mettez en cache les exceptions, vous ne pouvez vous débarrasser que de cette partie du coût des performances.
Les coûts de dépilage de 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 des gestionnaires d'exceptions nous aide probablement à avoir plus de chance.
Si nous éliminons les deux effets, le coût de la performance des exceptions est celui de la branche locale. Même si cela semble beau, cela ne signifie pas que vous devriez utiliser Exceptions comme flux de contrôle habituel, car dans ce cas vous êtes à la merci d'optimiser le compilateur! Vous ne devriez 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.
La règle empirique optimiste semble être 10 ^ -4 la fréquence des exceptions est suffisamment exceptionnelle. Cela dépend, bien sûr, du poids lourd des exceptions elles-mêmes, des actions exactes prises par les gestionnaires d'exceptions, etc.
Le résultat est que, lorsqu'une exception n'est pas générée, vous ne payez aucun coût. Ainsi, lorsque la condition exceptionnelle est suffisamment rare, la gestion des exceptions est plus rapide que d'utiliser un if
à chaque fois. L'article complet vaut vraiment la peine d'être lu.
Ma réponse, malheureusement, est trop longue pour être publiée ici. Alors permettez-moi de résumer ici et de vous renvoyer à http://www.fuwjax.com/how-slow-are-Java-exceptions/ pour les détails précis.
La vraie question ici n'est pas "Quelle est la lenteur des" échecs signalés en tant qu'exceptions "par rapport à" du 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 en tant qu'exceptions "par rapport aux échecs signalés d'autres manières?" En règle générale, les deux autres moyens de signaler les échecs sont les valeurs sentinelles ou les encapsuleurs de résultats.
Les valeurs Sentinel sont une tentative de retourner une classe en cas de succès et une autre en cas d'échec. Vous pouvez penser que cela revient presque à renvoyer une exception au lieu d'en lancer une. Cela nécessite une classe parente partagée avec l'objet success, puis une vérification "instanceof" et quelques conversions pour obtenir les informations de réussite ou d'échec.
Il s'avère que, au risque de la sécurité du type, les valeurs de Sentinel sont plus rapides que les exceptions, mais seulement environ 2 fois plus. Cela peut sembler beaucoup, mais cela 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 présenté ailleurs dans cette page.
Les wrappers de résultats, en revanche, ne sacrifient pas du tout la sécurité du type. Ils encapsulent les informations de réussite et d'échec dans une seule classe. Ainsi, au lieu de "instanceof", ils fournissent un "isSuccess ()" et des accesseurs pour les objets succès et échec. Cependant, les objets de résultat sont environ 2x plus lents que les exceptions. Il s'avère que créer un nouvel objet wrapper à chaque fois coûte beaucoup plus cher que de lancer une exception parfois.
En plus de cela, le langage fourni indique comment 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 fonctionner toujours (principalement) et lesquelles sont supposées signaler les échecs.
Les exceptions sont plus sûres que les sentinelles, plus rapides que les objets de résultats et moins surprenantes que les autres. Je ne suggère pas que try/catch remplace if si/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 moyens les plus fréquents d’atteindre un impact significatif sur les performances que je rencontre 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 parfois une exception ou créer un autre objet tout le temps, créez l'exception.
J'ai étendu les réponses données par @ Mecki et @ incarnate , sans 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 renvoient des valeurs sont encore plus rapides que les méthodes renvoyant des exceptions. IMHO, nous ne pouvons pas concevoir une API claire utilisant simplement des types de retour pour les flux de succès et d'erreur. Les méthodes qui lèvent 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);
}
}
Il y a quelque temps, j'ai écrit une classe pour tester la performance relative de la conversion de chaînes en entiers en utilisant deux approches: (1) appeler Integer.parseInt () et attraper l'exception, ou (2) faire correspondre la chaîne à une expression régulière et appeler parseInt () seulement si le match réussit. J'ai utilisé l'expression rationnelle de la manière la plus efficace possible (c'est-à-dire, en créant les objets Pattern et Matcher avant d'interagir dans la boucle) et je n'ai ni imprimé ni enregistré les stacktraces 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, l'expression régulière était deux fois plus rapide que parseInt (). Et si 20% étaient valides, ce qui signifie que l'exception était levée et capturé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 les chaînes valides: une fois pour la correspondance et une autre fois pour parseInt (). Mais lancer et attraper des exceptions compensait largement ce problème. Ce genre de situation ne se produira probablement pas très souvent dans le monde réel, mais si c'est le cas, vous ne devez absolument pas utiliser la technique de capture d'exception. Mais si vous ne validez que les entrées de l'utilisateur ou quelque chose du genre, utilisez certainement l'approche parseInt ().
Je ne sais pas si ces sujets sont liés, mais jadis j’ai voulu implémenter une astuce en nous basant sur la trace de pile du thread actuel: je voulais découvrir le nom de la méthode, ce qui a déclenché l’instanciation dans la classe instanciée (oui, 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).
Donc, Java Throwable
possède une méthode native fillInStackTrace
. Je pense que le bloc killer -catch
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 fonctions sont compilées dans la machine virtuelle Java à l'aide de ControlThrowable
, qui étend Throwable
et remplace sa fillInStackTrace
de la manière suivante:
override def fillInStackTrace(): Throwable = this
J'ai donc adapté le test ci-dessus (le nombre de cycles est réduit 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:
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 method3
et method4
est qu’ils génèrent différents types d’exceptions. Oui, method4
est toujours plus lent que method1
et method2
, mais la différence est beaucoup plus acceptable.
Je pense que le premier article fait référence à l'acte consistant à parcourir la pile d'appels et à créer une trace de pile comme étant la partie coûteuse. Bien que le second article ne le dise pas, je pense que c'est la partie la plus chère de la création d'objet. John Rose a n 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 trace de pile, etc.)
Mais quand même - je pense que cela ne devrait être considéré que comme un mal nécessaire, un dernier recours. La raison pour cela est d'imiter des fonctionnalités dans d'autres langues qui ne sont pas (encore) disponibles dans la JVM. Vous ne devriez PAS prendre l'habitude d'utiliser des exceptions pour contrôler le flux. Surtout pas pour des raisons de performance! Comme vous le mentionnez vous-même en n ° 2, vous risquez de masquer de graves bugs de votre code de cette façon, et il sera plus difficile à maintenir pour les nouveaux programmeurs.
Les micro-repères dans Java sont étonnamment difficiles à comprendre (on me l'a dit), en particulier lorsque vous arrivez en territoire JIT. Je doute donc que l'utilisation d'exceptions soit plus rapide que le "retour" dans la vie réelle. Par exemple, je soupçonne que vous avez entre 2 et 5 images de pile 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 pile qui fait plusieurs pages.
Peut-être pourriez-vous poster votre code de test?
J'ai effectué des tests de performances avec JVM 1.5 et l'utilisation d'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 les exceptions. Une boucle trivialement petite qui devait attraper l'exception a vu une augmentation de 2x son temps personnel.
J'ai vu des chiffres similaires dans le code de production ainsi que des micro-repères.
Les exceptions doivent définitivement PAS être utilisé pour tout ce qui est appelé fréquemment. Lancer des milliers d'exceptions une seconde causerait un énorme goulot d'étranglement.
Par exemple, utilisez "Integer.ParseInt (...)" pour rechercher toutes les valeurs incorrectes 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 valeur incorrecte sur un formulaire d'interface graphique utilisateur, probablement pas si mal du point de vue des performances.
Que ce soit ou non une bonne pratique de conception, je vais suivre 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 valeurs incorrectes sont normales - utilisez un code d'erreur. En passant une valeur à une fonction d’utilité interne, les mauvaises valeurs doivent être filtrées par code appelant - utilisez une exception.
Même si le lancement d'une exception n'est pas lent, c'est toujours une mauvaise idée de lancer des exceptions pour le déroulement normal du programme. Utilisé de cette façon, il est analogue à un GOTO ...
Je suppose que cela ne répond pas vraiment à la question cependant. J'imaginais que la sagesse "conventionnelle" de générer des exceptions avec lenteur était vraie dans les versions antérieures de Java (<1.4). La création d'une exception nécessite que VM crée l'intégralité de la trace de pile. Depuis lors, beaucoup de choses ont changé 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, tant qu'il est entièrement en ligne. Cependant, les exceptions explicitement créées et celles qui ne sont pas supprimées prennent beaucoup de temps pour créer la trace de pile. Remplacez fillInStackTrace
pour voir comment cela peut affecter les performances.
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 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 ni de la probabilité de succès ou d’échec. Seul l'appelant a cette information. En essayant d'éviter les exceptions, on aboutit à des idoms peu clairs sur les API, dans lesquels, dans certains cas, nous ne disposons que de versions d'exception pures mais lentes, et dans d'autres cas d'erreurs de valeur renvoyées rapides mais maladroites, et dans d'autres cas, nous aboutissons aux deux. . L’implémenteur de la bibliothèque peut être amené à écrire et gérer deux versions d’API, et l’appelant doit choisir l’une des deux versions à utiliser dans chaque situation.
C'est un peu le bordel. 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 que des mécanismes d'exception soient mis en œuvre 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 que nous revenons dans le code sensible à la performance.
Voici un exemple de code qui compare les performances des exceptions aux performances des valeurs d'erreur et de retour.
classe publique 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
Vérifier et propager les valeurs de retour ajoute un coût par rapport à l'appel baseline-null, et ce coût est proportionnel à la profondeur de l'appel. À une profondeur de 8 de la chaîne d'appels, la version de vérification des valeurs d'erreur renvoyées était environ 27% plus lente que la version de base qui ne vérifiait pas les valeurs renvoyées.
La performance d’exception, en comparaison, n’est pas fonction de la profondeur d’appel, mais de la fréquence des exceptions. Cependant, la dégradation à mesure que la fréquence des exceptions augmente est beaucoup plus spectaculaire. À une fréquence d'erreur de 25% seulement, le code a été exécuté 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 faisons les mauvais compromis dans nos implémentations d'exception. Les exceptions pourraient être plus rapides, soit en évitant des démarches coûteuses, soit en les transformant directement en contrôle de valeur de retour pris en charge par le compilateur. Jusqu'à ce qu'ils le fassent, nous sommes obligés de les éviter lorsque nous voulons que notre code soit exécuté rapidement.
Comparons simplement, disons, Integer.parseInt à la méthode suivante, qui renvoie simplement une valeur par défaut dans le cas de données non analysables au lieu de générer 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 aux données "valides", elles fonctionneront à peu près au même débit (même si Integer.parseInt parvient à gérer des données plus complexes). Mais dès que vous essayez d'analyser des données non valides (par exemple, d'analyser "abc" 1 000 000 de fois), la différence de performances doit être essentielle.
Great post sur les performances exceptionnelles 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
En fonction de la profondeur de 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 plus de détails (y compris l'assembleur x64 de JIT), lisez l'article du blog original.
Cela signifie que Hibernate/Spring/etc-EE-shit sont lents à cause des exceptions (xD) et de la réécriture du flux de contrôle des applications à partir des exceptions (remplacez-le par continure
/break
et restituez boolean
flags comme en C depuis l'appel de méthode) améliorer les performances de votre application 10x-100x, en fonction de la fréquence à laquelle vous les lancez))
J'ai changé la réponse de @Mecki ci-dessus pour que method1 renvoie 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, method1 était toujours le plus rapide ou aussi rapide que method2.
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:
Run 1
method1 took 841 ms, result was 2
method2 took 841 ms, result was 2
method3 took 85058 ms, result was 2
Run 2
method1 took 821 ms, result was 2
method2 took 838 ms, result was 2
method3 took 85929 ms, result was 2