L’impact sur les performances de l’utilisation d’instanceof en Java

Je travaille sur une application et une approche de conception implique une utilisation extrêmement intensive de l’opérateur d’ instanceof . Bien que je sache que la conception OO essaie généralement d’éviter d’utiliser instanceof , c’est une autre histoire et cette question est purement liée aux performances. Je me demandais s’il y avait un impact sur la performance? Est-ce que c’est aussi rapide que == ?

Par exemple, j’ai une classe de base avec 10 sous-classes. Dans une seule fonction qui prend la classe de base, je vérifie si la classe est une instance de la sous-classe et effectue une routine.

Une des autres façons de le résoudre était d’utiliser une primitive entière “type id”, et d’utiliser un masque de bits pour représenter les catégories des sous-classes, puis de simplement comparer les sous-classes “id type” à un masque de bits. masque constant représentant la catégorie.

Est-ce instanceof est en quelque sorte optimisé par la JVM pour être plus rapide que cela? Je veux m’en tenir à Java, mais les performances de l’application sont essentielles. Ce serait cool si quelqu’un sur cette route auparavant pouvait offrir des conseils. Est-ce que je saisis trop ou que je me concentre sur la mauvaise chose à optimiser?

Les compilateurs JVM / JIC modernes ont supprimé les performances de la plupart des opérations traditionnellement “lentes”, y compris les instances, la gestion des exceptions, la reflection, etc.

Comme l’a écrit Donald Knuth, «nous devrions oublier les petites efficacités, disons environ 97% du temps: l’optimisation prématurée est la racine de tout mal». Les performances de instanceof ne seront probablement pas un problème, alors ne perdez pas votre temps à trouver des solutions de rechange exotiques tant que vous n’êtes pas sûr que ce soit le problème.

Approche

J’ai écrit un programme de référence pour évaluer différentes implémentations:

  1. instanceof mise en œuvre (comme référence)
  2. object orienté via une classe abstraite et @Override une méthode de test
  3. en utilisant une implémentation de type propre
  4. getClass() == _.class

J’ai utilisé jmh pour exécuter le benchmark avec 100 appels de réchauffement, 1000 itérations sous- mesurées et 10 forks. Ainsi, chaque option a été mesurée avec 10 000 fois, ce qui prend 12:18:57 pour exécuter l’intégralité du benchmark sur mon MacBook Pro avec macOS 10.12.4 et Java 1.8. Le benchmark mesure le temps moyen de chaque option. Pour plus de détails, voir mon implémentation sur GitHub .

Par souci d’exhaustivité: il existe une version précédente de cette réponse et de mon benchmark .

Résultats

 |  Opération |  Temps d'exécution en nanosecondes par opération |  Relatif à instanceof |
 | ------------ | ------------------------------------ - | ------------------------ |
 |  INSTANCEOF |  39,598 ± 0,022 ns / op |  100,00% |
 |  GETCLASS |  39,687 ± 0,021 ns / op |  100,22% |
 |  TYPE |  46,295 ± 0,026 ns / op |  116,91% |
 |  OO |  48,078 ± 0,026 ns / op |  121,42% |

tl; dr

Dans Java 1.8 instanceof est l’approche la plus rapide, bien que getClass() soit très proche.

Je viens de faire un test simple pour voir comment les performances d’instanceOf se comparent à un simple appel s.equals () à un object chaîne avec une seule lettre.

dans une boucle de 10.000.000, l’instanceOf m’a donné 63-96ms, et la chaîne égale à 106-230ms

J’ai utilisé java jvm 6.

Donc, dans mon test simple, il est plus rapide de faire une instanceOf au lieu d’une comparaison de chaîne à un caractère.

L’utilisation de .equals () d’Integer au lieu de ssortingng m’a donné le même résultat, seulement quand j’ai utilisé le == j’étais plus rapide que instanceOf de 20ms (dans une boucle de 10.000.000)

Les éléments qui détermineront l’impact sur la performance sont les suivants:

  1. Le nombre de classes possibles pour lesquelles l’opérateur instanceof pourrait retourner true
  2. La dissortingbution de vos données – la plupart des opérations d’instance sont-elles résolues lors de la première ou de la deuxième tentative? Vous voudrez mettre vos plus susceptibles de retourner les opérations vraies en premier.
  3. L’environnement de déploiement L’exécution sur une machine virtuelle Sun Solaris est très différente de celle de la machine virtuelle Java Windows. Solaris s’exécutera par défaut en mode “serveur”, tandis que Windows s’exécutera en mode client. Les optimisations JIT sur Solaris rendront tous les access aux méthodes identiques.

J’ai créé un microbenchmark pour quatre méthodes d’expédition différentes . Les résultats de Solaris sont les suivants, le plus petit nombre étant plus rapide:

 InstanceOf 3156 class== 2925 OO 3083 Id 3067 

Répondre à votre toute dernière question: à moins qu’un profileur ne vous dise que vous consacrez une quantité de temps ridicule à une instance de ce type:

Avant de vous interroger sur l’optimisation de quelque chose qui n’a jamais besoin d’être optimisé: écrivez votre algorithme de la manière la plus lisible et exécutez-le. Exécutez-le jusqu’à ce que le compilateur Jit ait la possibilité de l’optimiser lui-même. Si vous rencontrez alors des problèmes avec ce morceau de code, utilisez un profileur pour vous dire où gagner le plus et optimiser cela.

En période de forte optimisation des compilateurs, vos suppositions sur les goulots d’étranglement seront probablement complètement fausses.

Et dans l’esprit de cette réponse (ce que je crois de tout mon cœur): Je ne sais absolument pas comment instanceof et == se rapportent une fois que le compilateur Jit a eu la chance de l’optimiser.

J’ai oublié: ne jamais mesurer la première course.

J’ai la même question, mais comme je n’ai pas trouvé de «mésortingques de performance» pour les cas d’utilisation similaires aux miens, j’ai fait quelques exemples de code. Sur mon matériel et Java 6 et 7, la différence entre instance et activation de 10mln est

 for 10 child classes - instanceof: 1200ms vs switch: 470ms for 5 child classes - instanceof: 375ms vs switch: 204ms 

Ainsi, instanceof est vraiment plus lent, en particulier sur un grand nombre d’instructions if-else-if, mais la différence sera négligeable dans une application réelle.

 import java.util.Date; public class InstanceOfVsEnum { public static int c1, c2, c3, c4, c5, c6, c7, c8, c9, cA; public static class Handler { public enum Type { Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9, TypeA } protected Handler(Type type) { this.type = type; } public final Type type; public static void addHandlerInstanceOf(Handler h) { if( h instanceof H1) { c1++; } else if( h instanceof H2) { c2++; } else if( h instanceof H3) { c3++; } else if( h instanceof H4) { c4++; } else if( h instanceof H5) { c5++; } else if( h instanceof H6) { c6++; } else if( h instanceof H7) { c7++; } else if( h instanceof H8) { c8++; } else if( h instanceof H9) { c9++; } else if( h instanceof HA) { cA++; } } public static void addHandlerSwitch(Handler h) { switch( h.type ) { case Type1: c1++; break; case Type2: c2++; break; case Type3: c3++; break; case Type4: c4++; break; case Type5: c5++; break; case Type6: c6++; break; case Type7: c7++; break; case Type8: c8++; break; case Type9: c9++; break; case TypeA: cA++; break; } } } public static class H1 extends Handler { public H1() { super(Type.Type1); } } public static class H2 extends Handler { public H2() { super(Type.Type2); } } public static class H3 extends Handler { public H3() { super(Type.Type3); } } public static class H4 extends Handler { public H4() { super(Type.Type4); } } public static class H5 extends Handler { public H5() { super(Type.Type5); } } public static class H6 extends Handler { public H6() { super(Type.Type6); } } public static class H7 extends Handler { public H7() { super(Type.Type7); } } public static class H8 extends Handler { public H8() { super(Type.Type8); } } public static class H9 extends Handler { public H9() { super(Type.Type9); } } public static class HA extends Handler { public HA() { super(Type.TypeA); } } final static int cCycles = 10000000; public static void main(Ssortingng[] args) { H1 h1 = new H1(); H2 h2 = new H2(); H3 h3 = new H3(); H4 h4 = new H4(); H5 h5 = new H5(); H6 h6 = new H6(); H7 h7 = new H7(); H8 h8 = new H8(); H9 h9 = new H9(); HA hA = new HA(); Date dtStart = new Date(); for( int i = 0; i < cCycles; i++ ) { Handler.addHandlerInstanceOf(h1); Handler.addHandlerInstanceOf(h2); Handler.addHandlerInstanceOf(h3); Handler.addHandlerInstanceOf(h4); Handler.addHandlerInstanceOf(h5); Handler.addHandlerInstanceOf(h6); Handler.addHandlerInstanceOf(h7); Handler.addHandlerInstanceOf(h8); Handler.addHandlerInstanceOf(h9); Handler.addHandlerInstanceOf(hA); } System.out.println("Instance of - " + (new Date().getTime() - dtStart.getTime())); dtStart = new Date(); for( int i = 0; i < cCycles; i++ ) { Handler.addHandlerSwitch(h1); Handler.addHandlerSwitch(h2); Handler.addHandlerSwitch(h3); Handler.addHandlerSwitch(h4); Handler.addHandlerSwitch(h5); Handler.addHandlerSwitch(h6); Handler.addHandlerSwitch(h7); Handler.addHandlerSwitch(h8); Handler.addHandlerSwitch(h9); Handler.addHandlerSwitch(hA); } System.out.println("Switch of - " + (new Date().getTime() - dtStart.getTime())); } } 

instanceof est vraiment rapide, ne prenant que quelques instructions sur le processeur.

Apparemment, si une classe X ne possède pas de sous-classes chargées (JVM sait), instanceof peut être optimisé comme suit:

  x instanceof X ==> x.getClass()==X.class ==> x.classID == constant_X_ID 

Le coût principal est juste une lecture!

Si X possède des sous-classes chargées, quelques lectures supplémentaires sont nécessaires. ils sont probablement co-localisés, de sorte que le coût supplémentaire est très faible.

Bonnes nouvelles tout le monde!

instanceof va probablement être plus coûteux qu’un simple équivalent dans la plupart des implémentations du monde réel (c’est-à-dire celles où instanceof est vraiment nécessaire, et vous ne pouvez pas simplement le résoudre en remplaçant une méthode commune, comme chaque manuel Demian ci-dessus suggère).

Pourquoi donc? Parce que ce qui va probablement se passer est que vous avez plusieurs interfaces, qui fournissent des fonctionnalités (disons, les interfaces x, y et z), et certains objects à manipuler qui peuvent (ou non) implémenter une de ces interfaces … mais Pas directement. Dis, par exemple, j’ai:

w étend x

Un outil w

B étend A

C étend B, implémente y

D étend C, implémente z

Supposons que je traite une instance de D, l’object d. Le calcul (d instanceof x) nécessite de prendre d.getClass (), boucle à travers les interfaces implémentées pour savoir si on est == à x, et si ce n’est pas le cas, à nouveau pour tous leurs ancêtres … Dans notre cas, Si vous faites une première exploration de cet arbre, vous obtiendrez au moins 8 comparaisons, en supposant que y et z ne prolongent rien …

La complexité d’un arbre de dérivation du monde réel est susceptible d’être plus élevée. Dans certains cas, le JIT peut en optimiser la plupart, s’il est capable de résoudre à l’avance d comme étant, dans tous les cas possibles, une instance de quelque chose qui étend x. De manière réaliste, cependant, vous allez parcourir la traversée de l’arbre la plupart du temps.

Si cela devient un problème, je suggérerais plutôt d’utiliser un mappage de gestionnaire, liant la classe concrète de l’object à une fermeture qui le manipule. Il supprime la phase de traversée de l’arbre en faveur d’un mappage direct. Cependant, attention, si vous avez défini un gestionnaire pour C.class, mon object d ci-dessus ne sera pas reconnu.

voici mes 2 centimes, j’espère qu’ils aident …

«instanceof» est en fait un opérateur, comme + ou -, et je pense qu’il possède sa propre instruction de bytecode JVM. Cela devrait être très rapide.

Je ne devrais pas que si vous avez un commutateur où vous testez si un object est une instance d’une sous-classe, alors votre conception devra peut-être être retravaillée. Pensez à pousser le comportement spécifique à la sous-classe dans les sous-classes elles-mêmes.

L’instanceof est très rapide. Il se résume à un bytecode utilisé pour la comparaison des références de classe. Essayez quelques millions d’instances en boucle et voyez par vous-même.

instanceof est très efficace, il est donc peu probable que votre performance en souffre. Cependant, l’utilisation de beaucoup d’instanceof suggère un problème de conception.

Si vous pouvez utiliser xClass == Ssortingng.class, c’est plus rapide. Note: vous n’avez pas besoin d’instanceof pour les classes finales.

Il est difficile de dire comment une certaine JVM implémente une instance de, mais dans la plupart des cas, les objects sont comparables aux structures et les classes le sont aussi et chaque structure d’object a un pointeur sur la structure de classe dont elle est une instance. Donc en fait instanceof pour

 if (o instanceof java.lang.Ssortingng) 

pourrait être aussi rapide que le code C suivant

 if (objectStruct->iAmInstanceOf == &java_lang_Ssortingng_class) 

en supposant qu’un compilateur JIT est en place et effectue un travail décent.

Considérant que cela ne concerne que l’access à un pointeur, obtenir un pointeur à un certain décalage vers lequel pointe le pointeur et le comparer à un autre pointeur (qui est fondamentalement le même que tester des nombres de 32 bits égaux), être très rapide

Cela n’a pas besoin, cependant, cela dépend beaucoup de la JVM. Cependant, si cela s’avérait être l’opération de goulot d’étranglement dans votre code, je considérerais plutôt l’implémentation de la JVM comme étant médiocre. Même celui qui ne possède pas de compilateur JIT et qui interprète uniquement du code devrait pouvoir effectuer un test d’instance en un rien de temps.

InstanceOf est un avertissement de mauvaise conception orientée object.

Les JVM actuelles signifient que l’ instanceOf n’est pas un problème de performance en soi. Si vous vous en servez beaucoup, en particulier pour les fonctionnalités de base, il est probablement temps d’examiner la conception. Les performances (et la simplicité / la facilité de maintenance) de la refactorisation pour une meilleure conception l’emporteront largement sur les cycles de processeur réels consacrés à l’appel même d’ instanceOf .

Pour donner un très petit exemple de programmation simpliste.

 if (SomeObject instanceOf Integer) { [do something] } if (SomeObject instanceOf Double) { [do something different] } 

Une architecture médiocre serait-elle un meilleur choix si SomeObject était la classe parente de deux classes enfants où chaque classe enfant remplaçait une méthode (doSomething) afin que le code se présente comme suit:

 Someobject.doSomething(); 

Demian et Paul mentionnent un bon point. Cependant , le placement du code à exécuter dépend vraiment de la manière dont vous souhaitez utiliser les données …

Je suis un grand fan des petits objects de données qui peuvent être utilisés de nombreuses manières. Si vous suivez l’approche de substitution (polymorphe), vos objects ne peuvent être utilisés que “dans un sens”.

C’est là que les motifs entrent en jeu …

Vous pouvez utiliser la double expédition (comme dans le modèle de visiteur) pour demander à chaque object de vous “appeler” en passant – ceci résoudra le type de l’object. Cependant (encore) vous aurez besoin d’une classe capable de “faire les choses” avec tous les sous-types possibles.

Je préfère utiliser un modèle de stratégie, dans lequel vous pouvez enregistrer des stratégies pour chaque sous-type que vous souhaitez gérer. Quelque chose comme ce qui suit. Notez que cela ne sert que pour les correspondances de type exactes, mais a l’avantage d’être extensible – les consortingbuteurs tiers peuvent append leurs propres types et gestionnaires. (Ceci est bon pour les frameworks dynamics comme OSGi, où de nouveaux bundles peuvent être ajoutés)

J’espère que cela inspirera d’autres idées …

 package com.javadude.sample; import java.util.HashMap; import java.util.Map; public class StrategyExample { static class SomeCommonSuperType {} static class SubType1 extends SomeCommonSuperType {} static class SubType2 extends SomeCommonSuperType {} static class SubType3 extends SomeCommonSuperType {} static interface Handler { Object handle(T object); } static class HandlerMap { private Map, Handler> handlers_ = new HashMap, Handler>(); public  void add(Class c, Handler handler) { handlers_.put(c, handler); } @SuppressWarnings("unchecked") public  Object handle(T o) { return ((Handler) handlers_.get(o.getClass())).handle(o); } } public static void main(Ssortingng[] args) { HandlerMap handlerMap = new HandlerMap(); handlerMap.add(SubType1.class, new Handler() { @Override public Object handle(SubType1 object) { System.out.println("Handling SubType1"); return null; } }); handlerMap.add(SubType2.class, new Handler() { @Override public Object handle(SubType2 object) { System.out.println("Handling SubType2"); return null; } }); handlerMap.add(SubType3.class, new Handler() { @Override public Object handle(SubType3 object) { System.out.println("Handling SubType3"); return null; } }); SubType1 subType1 = new SubType1(); handlerMap.handle(subType1); SubType2 subType2 = new SubType2(); handlerMap.handle(subType2); SubType3 subType3 = new SubType3(); handlerMap.handle(subType3); } } 

Généralement, la raison pour laquelle l’opérateur “instanceof” est mal vu dans un cas comme celui-ci (où l’instanceof vérifie les sous-classes de cette classe de base) est que vous devez déplacer les opérations dans une méthode et la remplacer par la méthode appropriée. sous-classes. Par exemple, si vous avez:

 if (o instanceof Class1) doThis(); else if (o instanceof Class2) doThat(); //... 

Vous pouvez le remplacer par

 o.doEverything(); 

et ensuite, l’implémentation de “doEverything ()” dans Class1 appelle “doThis ()”, et dans Class2, appelle “doThat ()”, et ainsi de suite.

Dans la version Java moderne, l’opérateur instanceof est plus rapide qu’un appel de méthode simple. Ça signifie:

 if(a instanceof AnyObject){ } 

est plus rapide que:

 if(a.getType() == XYZ){ } 

Une autre chose est si vous avez besoin de cascade de nombreuses instances. Ensuite, un commutateur qui appelle uniquement une fois getType () est plus rapide.

Si la vitesse est votre seul objective, alors l’utilisation des constantes int pour identifier les sous-classes semble se réduire d’une milliseconde de temps

 static final int ID_A = 0; static final int ID_B = 1; abstract class Base { final int id; Base(int i) { id = i; } } class A extends Base { A() { super(ID_A); } } class B extends Base { B() { super(ID_B); } } ... Base obj = ... switch(obj.id) { case ID_A: .... break; case ID_B: .... break; } 

conception OO terrible, mais si votre parsing de performance indique que c’est là que le goulot d’étranglement est peut-être alors. Dans mon code, le code d’expédition prend 10% du temps d’exécution total, ce qui a peut-être consortingbué à une amélioration de la vitesse totale de 1%.

Je reviendrai à vous sur l’instance de performance. Mais un moyen d’éviter le problème (ou son absence) serait de créer une interface parent pour toutes les sous-classes sur lesquelles vous devez faire instanceof. L’interface sera un super ensemble de toutes les méthodes des sous-classes pour lesquelles vous devez faire une vérification de l’instance. Lorsqu’une méthode ne s’applique pas à une sous-classe spécifique, fournissez simplement une implémentation factice de cette méthode. Si je ne me suis pas mépris sur le problème, c’est comme ça que je me suis retrouvé dans le passé.

Vous devez mesurer / profil si c’est vraiment un problème de performance dans votre projet. Si c’est le cas, je recommande une nouvelle conception – si possible. Je suis sûr que vous ne pouvez pas battre l’implémentation native de la plateforme (écrite en C). Vous devriez également considérer l’inheritance multiple dans ce cas.

Vous devriez en dire plus sur le problème, vous pourriez peut-être utiliser un magasin associatif, par exemple un object Map si vous n’êtes intéressé que par les types concrets.

En ce qui concerne la remarque de Peter Lawrey selon laquelle vous n’avez pas besoin d’instanceof pour les classes finales et que vous pouvez simplement utiliser une égalité de référence, faites attention! Même si les classes finales ne peuvent pas être étendues, leur chargement par le même chargeur de classes n’est pas garanti. N’utilisez que x.getClass () == SomeFinal.class ou ses semblables si vous êtes absolument certain qu’il n’y a qu’un seul chargeur de classe en jeu pour cette section de code.

Je préfère également une approche enum, mais j’utiliserais une classe de base abstraite pour forcer les sous-classes à implémenter la méthode getType() .

 public abstract class Base { protected enum TYPE { DERIVED_A, DERIVED_B } public abstract TYPE getType(); class DerivedA extends Base { @Override public TYPE getType() { return TYPE.DERIVED_A; } } class DerivedB extends Base { @Override public TYPE getType() { return TYPE.DERIVED_B; } } } 

Je pensais que cela pourrait valoir la peine de présenter un contre-exemple au consensus général sur cette page, selon lequel “instanceof” ne soit pas assez coûteux pour vous inquiéter. J’ai trouvé un code dans une boucle interne qui (dans une tentative historique d’optimisation)

 if (!(seq instanceof SingleItem)) { seq = seq.head(); } 

où l’appel à head () sur un SingleItem renvoie la valeur inchangée. Remplacer le code par

 seq = seq.head(); 

Cela me donne une accélération de 269ms à 169ms, malgré le fait qu’il se passe des choses assez lourdes dans la boucle, comme la conversion de chaîne en double. Il est bien sûr possible que l’accélération soit plus due à l’élimination de la twig conditionnelle qu’à la suppression de l’instance opérateur elle-même; mais je pensais que ça valait la peine de le mentionner.

Vous vous concentrez sur la mauvaise chose. La différence entre instanceof et toute autre méthode de vérification de la même chose ne serait probablement même pas mesurable. Si les performances sont critiques, Java est probablement le mauvais langage. La principale raison étant que vous ne pouvez pas contrôler le moment où la VM décide de collecter des ordures, ce qui peut amener le processeur à 100% pendant plusieurs secondes dans un programme volumineux (MagicDraw 10 était parfait pour cela). À moins que vous ne maîsortingsiez tous les ordinateurs sur lesquels ce programme s’exécutera, vous ne pouvez pas garantir la version de JVM sur laquelle il sera installé, et beaucoup d’anciens ont des problèmes de vitesse majeurs. Si c’est une petite application, vous pouvez être d’accord avec Java, mais si vous lisez et supprimez constamment des données, vous remarquerez que le GC se lance.