juin
2006
On entends parfois dire que l’utilisation de bloc try/catch pouvait être pénalisant en terme de performance dans une application. Si bien que certain préfèrent retourner un code d’erreur plutôt que de lever une Exception…
J’ai voulu déterminer quel était la part de vérité et de d’estimer si cela est vraiment pénalisant dans une application…
J’ai donc comparé les performances d’une méthode dont la gestion des erreurs est basée sur le principe des exceptions, avec une autre qui se contente de retourner un code d’erreur.
Le code de ces méthodes est le suivant :
public int methodReturnInt (String param) { if (param==null) { return -1; } // traitement return 0; } public void methodThrowsException (String param) throws Exception { if (param==null) { throw new Exception("La valeur 'null' n'est pas acceptée"); } // traitement }
Ces deux méthodes renverront une erreur si elles reçoivent une valeur null en paramètre. Et la méthode suivante permettra de tester les performances de ces deux méthodes :
public void testPerf (String param, int iteration) { long start; long duree; System.out.println(" * " + iteration + " appels de methode avec '" + param + "': "); System.out.print("\t methodReturnInt : "); start = System.currentTimeMillis(); for (int i=0; i<iteration; i++) { switch (methodReturnInt(param)) { case -1: // Traitement Erreur break; case 0: // Traitement OK break; } } duree = System.currentTimeMillis()-start; System.out.println( duree + " ms."); System.out.print("\t methodThrowsException : "); start = System.currentTimeMillis(); for (int i=0; i<iteration; i++) { try { methodThrowsException(param); // Traitement OK } catch (Exception e){ // Traitement Erreur } } duree = System.currentTimeMillis()-start; System.out.println( duree + " ms."); System.out.println(); }
Premier constat : les deux méthodes ont un code assez proche : les différences d’écritures sont assez minimes, si ce n’est que l’utilisation des exceptions permet de ‘libérer’ le retour de la méthode. Sinon on ne peut pas vraiment dire qu’une est beaucoup plus lisible que l’autre…
Enfin on utilisera le code suivant pour lancer le test avec 5 millions d’itérations, dans les deux cas possibles (avec ou sans erreur) :
public static void main(String[] args) { ExceptionTester obj = new ExceptionTester(); int iteration = 5000000; obj.testPerf("string", iteration); obj.testPerf(null, iteration); }
Ce qui nous donne à l’exécution :
* 5000000 appels de methode avec 'string': methodReturnInt : 31 ms. methodThrowsException : 31 ms. * 5000000 appels de methode avec 'null': methodReturnInt : 31 ms. methodThrowsException : 20188 ms.
Si aucune erreur n’est renvoyé (premier cas), il n’y a quasiment pas de différences entre les deux méthodes. Par contre, en cas d’erreur, on se retrouve avec 20 secondes de différences. Enorme !!!
Mais il ne faut pas s’arrêter à ce premier résultat, et comprendre la raison d’une telle différence : on peut déjà en déduire que cette différence ne vient pas du bloc try/catch puisque dans le premier cas il n’y a pas de différence. Cette différence provient plutôt de la création de l’exception.
La faute revient en effet à la méthode fillInStackTrace() hérité de la classe Throwable par toutes les exceptions. Cette méthode permet en effet de retrouver la pile des appels jusqu’à l’origine de l’exception, et facilite grandement la correction des exceptions. Toutefois cela implique un coût supplémentaire causé par l’allocation des ressources… Or il se trouve que cette méthode est toujours appelée par le constructeur de Throwable, et donc également pour toutes les exceptions…
La seule solution pour ‘désactiver’ la création du ‘Stack-Trace’ est de redéfinir la méthode dans la classe de l’exception fille, par exemple :
public class NoStackTraceException extends Exception { public NoStackTraceException(String message) { super(message); } public synchronized Throwable fillInStackTrace() { // on ne fait rien return this; } }
En rajoutant un test avec une méthode qui renvoi cette exception, on obtient désormais le résultat suivant :
* 5000000 appels de methode avec 'string': methodReturnInt : 31 ms. methodThrowsNoStack : 31 ms. methodThrowsException : 16 ms. * 5000000 appels de methode avec 'null': methodReturnInt : 31 ms. methodThrowsNoStack : 1828 ms. methodThrowsException : 20063 ms.
On est 10 fois plus performant qu’avec une exception normal, mais on reste quand même beaucoup plus lent que la méthode qui renvoi une valeur entière… Et cette fois la raison revient à la création de l’exception elle-même. En effet, 5 millions d’exceptions sont créées, ce qui fait que la mémoire est vite saturé et que le Garbage Collector passe son temps à allouer/désallouer des objets… D’ailleurs, si on retourne un objet à la place d’un int, on se retrouve alors avec un temps d’exécution proche :
* 5000000 appels de methode avec 'string': methodReturnInt : 47 ms. methodReturnObject : 47 ms. methodThrowsNoStack : 31 ms. methodThrowsException : 31 ms. * 5000000 appels de methode avec 'null': methodReturnInt : 31 ms. methodReturnObject : 1531 ms. methodThrowsNoStack : 1891 ms. methodThrowsException : 19344 ms.
Au final, les exceptions restent moins performantes que de retourner un simple int, ce qui est tout à fait logique : la différence entre les deux vient de l’allocation des ressources nécessaires aux exceptions. Or pour être utile, une erreur doit remonter un maximum d’information… En retournant un int, le développeur doit se charger de récupérer ces informations lors du traitement de l’erreur, et donc allouer des ressources… ce qui ne fait que reporter le ‘problème’ un peu plus loin dans le code…
Il se trouve que cette allocation n’est coûteuse qu’à cause du grand nombre d’erreur généré en peu de temps. Et dans 95% des cas, les méthodes ne renvoient pas (ou peu) d’exception dans leur déroulement normal… En tout cas pas 5 millions d’exceptions dans une seule boucle…
Et bien entendu je n’ai pas choisit cette valeur de 5 millions d’itérations au hasard. En effet, cette valeur met en évidence le problème en faisant ‘exploser’ le temps d’exécution à cause du grand nombre d’exceptions créées, mais dès que l’on diminue cette valeur, on retrouve des différences plus minimes (environ 1,7 secondes pour 500 000 exceptions, 250 ms pour 50 000 exceptions, et 30 ms pour 5 000 exceptions).
Donc il serait vraiment dommage de se passer de la gestion des exceptions… surtout que cette dernière apporte de nombreux avantages :
- Elles contiennent de nombreuses informations sur l’erreur (message d’erreur et ‘Stack Trace’),
- Elles permettent d’obliger le développeur à les traiter, alors que rien ne l’empêche d’ignorer un code de retour,
- On peut facilement sortir proprement de la méthode en utilisant le mot-clef finally (pour fermer des connections ou fichiers par exemples),
- On peut rajouter de nouveaux types d’ exceptions sans impacter le code des méthodes appelantes (grâce au principe du polymorphisme),
Pour télécharger le code source Java utilisé dans cet article : ExceptionTester.java.
6 Commentaires + Ajouter un commentaire
Tutoriels
Discussions
- Définition exacte de @Override
- jre 1.5, tomcat 6.0 et multi processeurs
- Classes, méthodes private
- Recuperation du nom des parametres
- L'apparition du mot-clé const est-il prévu dans une version à venir du JDK?
- [REFLEXION] Connaitre toutes les classes qui implémentent une interface
- [ fuite ] memoire
- Possibilité d'accéder au type générique en runtime
- Difference de performances Unix/Windows d'un programme?
D’un point de vue général les exceptions n’ont d’impact… qu’en cas d’erreur Du coup, le fait qu’elles soient coûteuses n’est que très très très rarement un risque pour les performance.
Le seul cas que je vois, et là je rejoins d’une certaine mesure RanDomX (2 ans plus tard ;-), c’est lorsque le développeur confond exception et retour de fonction. Ne riez pas, j’ai déjà audité ce genre d’exotisme….
Cependant, le coût des exceptions étant dans la génération de la pile d’appel (à ne pas confondre avec la trace sous forme de chaîne), il y a un autre cas que l’on oublie qui est celui des traces. En effet, toutes utilisent le même mécanisme que le Throwable pour pouvoir afficher la position du LogRecord. Et là, les traces c’est quelque chose de beaucoup plus utilisé que les Exceptions…
C’est pour ca que je recommande à mes développeurs d’utiliser des types énumérés pour les erreurs « fonctionnelles » (cad les cas d’erreurs prévisible et faisant partis des cas standards).
>>
Sinon ce que je voulais montrer c’est que les exceptions n’impliquent une perte de performance que dans des cas extrêmes (5 millions d’exceptions), et qu’il y a donc peu d’intérêt à s’en passer lorsqu’il y en a besoin…
Je me disais bien qu’il y avait une raison
Un bon exemple pour marteller que pour convertir un String en INT on fait pas un cast dans un bloc try catch pour détecter si la chaine de caractère correspond à un INT
@piotrek
>> En .net le problème est… strictement identique
Il n’y a rien d’étonnant à cela… ce sont deux langages objets aux méchanismes assez proches dans les grandes lignes.
Sinon ce que je voulais montrer c’est que les exceptions n’impliquent une perte de performance que dans des cas extrêmes (5 millions d’exceptions), et qu’il y a donc peu d’intérêt à s’en passer lorsqu’il y en a besoin…
a++
En .net le problème est… strictement identique, la création de la stack trace (meme nom en plus) c’est une énorme concaténation d’informations en texte.
Personellement, si l’erreur n’est liée qu’à une cause mineure (valeur non specifiée par un utilisateur par ex qui ferait planter un bout de code) je crée un mechanisme en amont, avant le déclanchement du bug pour ne pas faire d’exception.
En cas de bug (un indice recupère par calcul, n’existe pas dans une collection) je traite avec les exceptions.
Ca semble logique, mais de plus, une fois l’exception lancée: plantée pour plantée, je me soucie absolument plus des problèmes de performances, j’hésite pas a recatcher l’erreur de base pour rajouter des couches d’exceptions par dessus, de logger copieusement etc…