El impacto en el rendimiento del uso de instanceof en Java

Estoy trabajando en una aplicación y un enfoque de diseño implica un uso extremadamente intenso del operador de instanceof . Si bien sé que el diseño OO generalmente trata de evitar el uso de instanceof , esa es una historia diferente y esta pregunta está puramente relacionada con el rendimiento. Me preguntaba si hay algún impacto en el rendimiento. ¿Es tan rápido como == ?

Por ejemplo, tengo una clase base con 10 subclases. En una sola función que toma la clase base, compruebo si la clase es una instancia de la subclase y realizo alguna rutina.

Una de las otras maneras en que pensé en resolverlo fue utilizar una primitiva de enteros “tipo id” y usar una máscara de bits para representar categorías de las subclases, y luego hacer una comparación de máscaras de bit de las subclases “tipo id” a una máscara constante que representa la categoría.

¿Es la instanceof algún modo optimizada por la JVM más rápida que eso? Quiero seguir con Java, pero el rendimiento de la aplicación es crítico. Sería genial si alguien que ha estado en este camino antes pudiera ofrecer algún consejo. ¿Debo criticar demasiado o centrarme en lo incorrecto para optimizar?

Los comstackdores JVM / JIC modernos han eliminado el impacto de rendimiento de la mayoría de las operaciones tradicionalmente “lentas”, incluidas instancia, gestión de excepciones, reflexión, etc.

Como escribió Donald Knuth, “deberíamos olvidarnos de las pequeñas eficiencias, digamos el 97% del tiempo: la optimización prematura es la raíz de todo mal”. El rendimiento de la instancia probablemente no sea un problema, así que no pierda el tiempo con soluciones exóticas hasta que esté seguro de que ese es el problema.

Enfoque

Escribí un progtwig de referencia para evaluar diferentes implementaciones:

  1. instanceof implementación (como referencia)
  2. objeto orientado a través de una clase abstracta y @Override un método de prueba
  3. utilizando una implementación de tipo propio
  4. getClass() == _.class implementación

Usé jmh para ejecutar el punto de referencia con 100 llamadas de calentamiento, 1000 iteraciones bajo medición y con 10 bifurcaciones. Así que cada opción se midió con 10 000 veces, lo que lleva 12:18:57 ejecutar toda la prueba comparativa en mi MacBook Pro con macOS 10.12.4 y Java 1.8. El punto de referencia mide el tiempo promedio de cada opción. Para más detalles, vea mi implementación en GitHub .

En aras de la exhaustividad: hay una versión anterior de esta respuesta y mi punto de referencia .

Resultados

 |  Operación |  Tiempo de ejecución en nanosegundos por operación |  Relativo a instanceof |
 | ------------ | ------------------------------------ - | ------------------------ |
 |  INSTANCEOF |  39,598 ± 0,022 ns / op |  100,00% |
 |  GETCLASS |  39,687 ± 0,021 ns / op |  100,22% |
 |  TIPO |  46,295 ± 0,026 ns / op |  116,91% |
 |  OO |  48,078 ± 0,026 ns / op |  121,42% |

tl; dr

En Java 1.8 instanceof es el enfoque más rápido, aunque getClass() está muy cerca.

Acabo de hacer una prueba simple para ver cómo el rendimiento de instanceOf se compara con una llamada simple de s.equals () a un objeto de cadena con una sola letra.

en un bucle de 10.000.000 el ejemplar Of me dio 63-96ms, y la cadena de equivalentes me dio 106-230ms

Usé java jvm 6.

Por lo tanto, en mi prueba simple es más rápido hacer una instancia en lugar de una comparación de cadena de un solo carácter.

Usar los .equals () de Integer en lugar de los de cadena me dieron el mismo resultado, solo cuando utilicé el == i fue más rápido que el instanceOf en 20ms (en un bucle de 10.000.000)

Los elementos que determinarán el impacto en el rendimiento son:

  1. El número de clases posibles para las que el operador de instanceof podría devolver true
  2. La distribución de sus datos: ¿la mayoría de las operaciones se resuelven en el primer o segundo bash? Querrá poner lo más probable para devolver operaciones verdaderas primero.
  3. El entorno de despliegue. Ejecutar en una máquina virtual Sun Solaris es significativamente diferente a la JVM de Sun de Windows. Solaris se ejecutará en modo ‘servidor’ de manera predeterminada, mientras que Windows se ejecutará en modo cliente. Las optimizaciones de JIT en Solaris harán que todos los métodos de acceso sean iguales.

Creé un microbenchmark para cuatro métodos diferentes de despacho . Los resultados de Solaris son los siguientes, y el número más pequeño es más rápido:

 InstanceOf 3156 class== 2925 OO 3083 Id 3067 

Respondiendo a su última pregunta: a menos que un generador de perfiles le diga que gasta una cantidad de tiempo ridícula en una instancia de: Sí, está chiflando.

Antes de preguntarse sobre la optimización de algo que nunca tuvo que ser optimizado: escriba su algoritmo de la manera más legible y ejecútelo. Ejecútelo, hasta que el comstackdor de jit tenga la oportunidad de optimizarlo. Si luego tiene problemas con este fragmento de código, use un generador de perfiles para indicarle dónde obtener más y optimizar esto.

En tiempos de comstackdores altamente optimizadores, es probable que sus conjeturas sobre los cuellos de botella sean completamente incorrectas.

Y con verdadero espíritu de esta respuesta (que creo de todo corazón): no sé cómo se asocian instantof y == una vez que el comstackdor de jit tuvo la oportunidad de optimizarlo.

Olvidé: nunca mida la primera carrera.

Tengo la misma pregunta, pero como no encontré “indicadores de rendimiento” para un caso de uso similar al mío, hice un código de muestra más. En mi hardware y Java 6 y 7, la diferencia entre instanceof y switch en 10mln iterations es

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

Por lo tanto, instanceof es realmente más lento, especialmente en una gran cantidad de sentencias if-else-if, sin embargo, la diferencia será insignificante en la aplicación real.

 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(String[] 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 es realmente rápido, tomando solo unas pocas instrucciones de la CPU.

Aparentemente, si una clase X no tiene subclases cargadas (JVM lo sabe), instanceof se puede optimizar como:

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

¡El costo principal es solo una lectura!

Si X tiene subclases cargadas, se necesitan algunas lecturas más; es probable que estén ubicados conjuntamente, por lo que el costo adicional es muy bajo también.

¡Buenas noticias para todos!

instanceof probablemente sea más costoso que un simple igual en la mayoría de las implementaciones del mundo real (es decir, aquellos en los que realmente se necesita instanceof, y no se puede resolver anulando un método común, como cada libro de texto para principiantes así como Demian arriba sugerir).

¿Porqué es eso? Porque lo que probablemente sucederá es que tiene varias interfaces, que proporcionan alguna funcionalidad (digamos, interfaces x, yyz) y algunos objetos para manipular que pueden (o no) implementar una de esas interfaces … pero no directamente. Digamos, por ejemplo, tengo:

w extiende x

A implementa w

B extiende A

C extiende B, implementa y

D extiende C, implementa z

Supongamos que estoy procesando una instancia de D, el objeto d. La computación (d instancia de x) requiere tomar d.getClass (), recorrer las interfaces que implementa para saber si uno es == a x, y si no lo hace de manera recursiva para todos sus antepasados ​​… En nuestro caso, si realiza una primera exploración de amplitud de ese árbol, arroja al menos 8 comparaciones, suponiendo que yy z no extienden nada …

La complejidad de un árbol de derivación del mundo real es probable que sea mayor. En algunos casos, el JIT puede optimizar la mayor parte de él, si es capaz de resolver por adelantado d como, en todos los casos posibles, una instancia de algo que se extiende x. Siendo realistas, sin embargo, vas a atravesar ese cruce de árboles la mayor parte del tiempo.

Si eso se convierte en un problema, sugeriría usar un mapa de controlador en su lugar, vinculando la clase concreta del objeto a un cierre que lo maneje. Elimina la fase transversal del árbol a favor de un mapeo directo. Sin embargo, tenga en cuenta que si ha configurado un controlador para C.class, mi objeto d anterior no será reconocido.

aquí están mis 2 centavos, espero que ayuden …

‘instanceof’ es en realidad un operador, como + o -, y creo que tiene su propia instrucción de bytecode JVM. Debería ser bastante rápido.

No debería, si tienes un interruptor donde estás probando si un objeto es una instancia de alguna clase secundaria, entonces tu diseño podría necesitar ser rediseñado. Considere empujar el comportamiento específico de subclase hacia abajo en las subclases.

Instanceof es muy rápido. Se reduce a un bytecode que se utiliza para la comparación de referencia de clase. Pruebe algunos millones de ejemplares en un ciclo y compruébelo usted mismo.

instanceof es muy eficiente, por lo que es poco probable que su rendimiento sufra. Sin embargo, usar muchos ejemplos sugiere un problema de diseño.

Si puede usar xClass == String.class, esto es más rápido. Nota: no necesita instanceof para las clases finales.

Es difícil decir cómo una determinada JVM implementa la instancia de, pero en la mayoría de los casos, los Objetos son comparables a las estructuras y las clases también y cada estructura de objeto tiene un puntero a la clase de la estructura de la que es una instancia. Entonces en realidad instancia de

 if (o instanceof java.lang.String) 

podría ser tan rápido como el siguiente código C

 if (objectStruct->iAmInstanceOf == &java_lang_String_class) 

asumiendo que un comstackdor JIT está en su lugar y hace un trabajo decente.

Teniendo en cuenta que esto solo está accediendo a un puntero, obteniendo un puntero con un cierto desplazamiento al que apunta el puntero y comparándolo con otro puntero (que es básicamente lo mismo que probar que los números de 32 bits sean iguales), diría que la operación puede ser muy rápido

Sin embargo, no tiene por qué depender mucho de la JVM. Sin embargo, si esta fuera la operación de cuello de botella en su código, consideraría que la implementación de JVM es bastante pobre. Incluso uno que no tenga un comstackdor JIT y solo interprete el código debería ser capaz de realizar una prueba de prueba prácticamente en un instante.

InstanceOf es una advertencia de diseño orientado a objetos pobre.

Las JVM actuales significan que el instanceOf no es una gran preocupación de rendimiento en sí mismo. Si te encuentras usándolo mucho, especialmente para la funcionalidad central, es probable que sea hora de mirar el diseño. Las ganancias de rendimiento (y simplicidad / mantenibilidad) de la refactorización para un mejor diseño superarán en gran medida los ciclos reales del procesador gastados en la llamada real instanceOf .

Para dar un pequeño ejemplo de progtwigción simplista.

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

Si una architecture es deficiente, una mejor opción hubiera sido tener SomeObject como clase padre de dos clases secundarias donde cada clase secundaria reemplaza un método (doSomething) para que el código se vea así:

 Someobject.doSomething(); 

Demian y Paul mencionan un buen punto; sin embargo , la ubicación del código para ejecutar realmente depende de cómo quiera usar los datos …

Soy un gran admirador de los pequeños objetos de datos que se pueden usar de muchas maneras. Si sigues el enfoque de anulación (polimórfica), tus objetos solo se pueden usar “de una sola manera”.

Aquí es donde entran los patrones …

Puede usar el doble despacho (como en el patrón de visitante) para pedir a cada objeto que se “pase”, esto resolverá el tipo de objeto. Sin embargo (nuevamente) necesitarás una clase que pueda “hacer cosas” con todos los posibles subtipos.

Prefiero usar un patrón de estrategia, donde puede registrar estrategias para cada subtipo que desea manejar. Algo como lo siguiente. Tenga en cuenta que esto solo ayuda con las coincidencias de tipo exacto, pero tiene la ventaja de que es extensible: los colaboradores de terceros pueden agregar sus propios tipos y controladores. (Esto es bueno para marcos dynamics como OSGi, donde se pueden agregar nuevos paquetes)

Espero que esto inspire algunas otras ideas …

 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(String[] 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); } } 

En general, la razón por la cual el operador “instanceof” es desaprobado en un caso como ese (donde el instalador está buscando subclases de esta clase base) es porque lo que debe hacer es mover las operaciones a un método y reemplazarlo por el apropiado subclases. Por ejemplo, si tienes:

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

Puedes reemplazar eso con

 o.doEverything(); 

y luego haga que la implementación de “doEverything ()” en Class1 llame a “doThis ()”, y en Class2 llame a “doThat ()”, y así sucesivamente.

En la versión moderna de Java, el operador instanceof es más rápido que una simple llamada a método. Esto significa:

 if(a instanceof AnyObject){ } 

es más rápido como:

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

Otra cosa es si necesita conectar en cascada muchos instanceof. Entonces, un interruptor que solo llama una vez getType () es más rápido.

Si su único objective es la velocidad, el uso de constantes int para identificar las subclases parece reducirse a milisegundos del tiempo.

 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; } 

terrible diseño de OO, pero si su análisis de rendimiento indica que es aquí donde se puede encontrar el cuello de botella es posible. En mi código, el código de envío toma el 10% del tiempo total de ejecución y esto puede contribuir a una mejora de la velocidad total del 1%.

Me pondré en contacto con usted en caso de ejecución. Pero una forma de evitar el problema (o la falta de él) sería crear una interfaz principal para todas las subclases en las que necesita hacer instanceof. La interfaz será un súper conjunto de todos los métodos en las subclases para los que debe hacer una comprobación de instancia. Cuando un método no se aplica a una subclase específica, simplemente proporcione una implementación ficticia de este método. Si no entendí mal el problema, así es como he solucionado el problema en el pasado.

Debería medir / perfilar si realmente es un problema de rendimiento en su proyecto. Si es así, recomendaría un rediseño, si es posible. Estoy bastante seguro de que no se puede superar la implementación nativa de la plataforma (escrita en C). También debería considerar la herencia múltiple en este caso.

Debería contar más sobre el problema, tal vez podría usar una tienda asociativa, por ejemplo, un Map si solo está interesado en los tipos concretos.

Con respecto a la nota de Peter Lawrey de que no necesita instanceof para las clases finales y solo puede usar una igualdad de referencia, ¡tenga cuidado! Aunque las clases finales no se pueden extender, no se garantiza que el mismo cargador de clases las cargue. Solo use x.getClass () == SomeFinal.class o su clase si está absolutamente seguro de que solo hay un cargador de clases en juego para esa sección de código.

También prefiero un enfoque enum, pero usaría una clase base abstracta para forzar a las subclases a implementar el método 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; } } } 

Pensé que podría valer la pena presentar un contraejemplo al consenso general en esta página que “instancia” no es lo suficientemente caro como para preocuparse. Descubrí que tenía un código en un bucle interno que (en algún bash histórico de optimización)

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

donde calling head () en un SingleItem devuelve el valor sin cambios. Reemplazando el código por

 seq = seq.head(); 

me da una aceleración de 269ms a 169ms, a pesar de que hay algunas cosas bastante pesadas sucediendo en el ciclo, como la conversión de cadena a doble. Es posible, por supuesto, que la aceleración se deba más a eliminar la twig condicional que a eliminar el operador de la instancia misma; pero pensé que valía la pena mencionarlo.

Te estás enfocando en lo incorrecto. La diferencia entre instanceof y cualquier otro método para verificar lo mismo probablemente no sea mensurable. Si el rendimiento es crítico, Java probablemente sea el idioma equivocado. La razón principal es que no puede controlar cuándo la máquina virtual decide que quiere recoger basura, lo que puede llevar a la CPU al 100% durante varios segundos en un progtwig grande (MagicDraw 10 fue excelente para eso). A menos que tenga el control de todas las computadoras en las que se ejecutará este progtwig, no puede garantizar en qué versión de JVM estará, y muchas de las anteriores tenían problemas importantes de velocidad. Si se trata de una aplicación pequeña, puede estar bien con Java, pero si está constantemente leyendo y descartando datos, entonces se dará cuenta cuando se active el GC.