java - una - para que se utiliza el operador instanceof
El impacto en el rendimiento del uso de instanceof en Java (23)
Enfoque
Escribí un programa de referencia para evaluar diferentes implementaciones:
-
instanceof
implementación (como referencia) - Objeto orientado a través de una clase abstracta y
@Override
un método de prueba - utilizando una implementación de tipo propio
-
getClass() == _.class
implementacion
jmh para ejecutar el punto de referencia con 100 llamadas de calentamiento, 1000 iteraciones bajo medición y con 10 bifurcaciones. Por lo tanto, cada opción se midió con 10 000 veces, lo que demora 12:18:57 para ejecutar todo el benchmark 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 integridad: hay una versión anterior de esta respuesta y mi punto de referencia .
Resultados
| Operation | Runtime in nanoseconds per operation | Relative to 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
En Java 1.8, instanceof
es el enfoque más rápido, aunque getClass()
está muy cerca.
Estoy trabajando en una aplicación y un enfoque de diseño implica un uso extremadamente intenso de la instanceof
operador. Aunque 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, verifico si la clase es una instancia de la subclase y realizo alguna rutina.
Una de las otras formas en las que pensé en resolverlo fue utilizar una primitiva entera de "id de tipo" en su lugar, y usar una máscara de bits para representar categorías de las subclases, y luego hacer una comparación de máscaras de bits de las subclases "id de tipo" a Máscara constante que representa la categoría.
¿La instanceof
de JVM está optimizada de alguna manera para ser más rápida que eso? Quiero seguir con Java, pero el rendimiento de la aplicación es fundamental. Sería genial si alguien que ha estado antes en este camino pudiera ofrecer algún consejo. ¿Estoy minando demasiado o concentrándome en algo incorrecto para optimizar?
''instanceof'' es en realidad un operador, como + o -, y creo que tiene su propia instrucción de bytecode de JVM. Debería ser bastante rápido.
No debería hacerlo si tiene un interruptor en el que está probando si un objeto es una instancia de alguna subclase, entonces es posible que deba volver a trabajar su diseño. Considere empujar el comportamiento específico de la subclase hacia las subclases.
Acabo de realizar una prueba simple para ver cómo se compara el rendimiento de instanceOf con una simple llamada a un objeto de cadena con una sola letra.
en un bucle de 10.000.000, el instanceOf me dio 63-96ms, y la cuerda igual a 106-230ms
Utilicé java jvm 6.
Por lo tanto, en mi prueba simple es más rápido hacer una comparación de secuencia de caracteres en lugar de una instancia.
el uso de .equals () en lugar de string me dio el mismo resultado, solo cuando usé == i era más rápido que instanceOf por 20ms (en un bucle de 10,000,000)
Con respecto a la nota de Peter Lawrey de que no necesita ejemplos para las clases finales y que solo puede usar una igualdad de referencia, ¡tenga cuidado! Aunque las clases finales no pueden extenderse, no se garantiza que sean cargadas por el mismo cargador de clases. Solo use x.getClass () == SomeFinal.class o su tipo si está absolutamente seguro de que solo hay un cargador de clases en juego para esa sección de código.
Debe medir / perfilar si realmente es un problema de rendimiento en su proyecto. Si es así, recomiendo 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 debe considerar la herencia múltiple en este caso.
Debería decir más sobre el problema, tal vez podría usar una tienda asociativa, por ejemplo, un Mapa <Clase, Objeto> si solo está interesado en los tipos concretos.
Demian y Paul mencionan un buen punto; Sin embargo , la ubicación del código a ejecutar realmente depende de cómo desea utilizar los datos ...
Soy un gran fanático de los objetos de datos pequeños que se pueden usar de muchas maneras. Si sigue el enfoque de anulación (polimórfico), sus objetos solo se pueden usar "de una manera".
Aquí es donde entran los patrones ...
Puede usar el envío doble (como en el patrón de visitante) para pedir a cada objeto que "se llame" y que pase por sí mismo; esto resolverá el tipo de objeto. Sin embargo (de nuevo) necesitará una clase que pueda "hacer cosas" con todos los subtipos posibles.
Prefiero usar un patrón de estrategia, donde puede registrar estrategias para cada subtipo que desee manejar. Algo como lo siguiente. Tenga en cuenta que esto solo ayuda a las coincidencias de tipo exactas, pero tiene la ventaja de que es extensible: los colaboradores externos pueden agregar sus propios tipos y controladores. (Esto es bueno para marcos dinámicos como OSGi, donde se pueden agregar nuevos paquetes)
Esperemos 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<T extends SomeCommonSuperType> {
Object handle(T object);
}
static class HandlerMap {
private Map<Class<? extends SomeCommonSuperType>, Handler<? extends SomeCommonSuperType>> handlers_ =
new HashMap<Class<? extends SomeCommonSuperType>, Handler<? extends SomeCommonSuperType>>();
public <T extends SomeCommonSuperType> void add(Class<T> c, Handler<T> handler) {
handlers_.put(c, handler);
}
@SuppressWarnings("unchecked")
public <T extends SomeCommonSuperType> Object handle(T o) {
return ((Handler<T>) handlers_.get(o.getClass())).handle(o);
}
}
public static void main(String[] args) {
HandlerMap handlerMap = new HandlerMap();
handlerMap.add(SubType1.class, new Handler<SubType1>() {
@Override public Object handle(SubType1 object) {
System.out.println("Handling SubType1");
return null;
} });
handlerMap.add(SubType2.class, new Handler<SubType2>() {
@Override public Object handle(SubType2 object) {
System.out.println("Handling SubType2");
return null;
} });
handlerMap.add(SubType3.class, new Handler<SubType3>() {
@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 que el operador "instanceof" está mal visto en un caso como ese (donde el instanceof está comprobando las subclases de esta clase base) es porque lo que debería estar haciendo es mover las operaciones a un método y anularlo por el adecuado subclases Por ejemplo, si tiene:
if (o instanceof Class1)
doThis();
else if (o instanceof Class2)
doThat();
//...
Puedes reemplazarlo con
o.doEverything();
y luego tener la implementación de "doEverything ()" en la llamada de Class1 "doThis ()", y en la llamada de Class2 "doThat ()", y así sucesivamente.
En la versión moderna de Java, el operador instanceof es más rápido como una simple llamada de método. Esto significa:
if(a instanceof AnyObject){
}
es más rápido que
if(a.getType() == XYZ){
}
Otra cosa es si necesitas conectar en cascada muchos ejemplos. Entonces, un interruptor que solo llama una vez getType () es más rápido.
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 objetos tiene un puntero a la estructura de la clase de la que es una instancia. Así que en realidad ejemplo para
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)
Suponiendo que un compilador JIT está en su lugar y hace un trabajo decente.
Teniendo en cuenta que solo se está accediendo a un puntero, obteniendo un puntero en 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 son iguales), diría que la operación puede realmente ser muy rapido
No es necesario, sin embargo, depende mucho de la JVM. Sin embargo, si esto resultara ser la operación de cuello de botella en su código, consideraría que la implementación de JVM es bastante deficiente. Incluso uno que no tenga compilador JIT y solo interprete el código debería poder realizar una instancia de prueba prácticamente en ningún momento.
Es probable que instanceof sea más costoso que uno simple en la mayoría de las implementaciones del mundo real (es decir, en las que realmente se necesita instanceof, y no se puede resolver simplemente al reemplazar un método común, como cada libro de texto para principiantes, así como Demian sugiere arriba).
¿Porqué es eso? Porque lo que probablemente sucederá es que tiene varias interfaces, que proporcionan alguna funcionalidad (digamos, interfaces x, y, z), 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 instanceof x) requiere tomar d.getClass (), recorrer las interfaces que implementa para saber si uno es == a x, y si no, volver a hacerlo de forma recursiva para todos sus antepasados ... En nuestro caso, Si hace una primera exploración amplia de ese árbol, produce al menos 8 comparaciones, suponiendo que y yz 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, si es capaz de resolver por adelantado d como si fuera, en todos los casos posibles, una instancia de algo que se extiende x. Sin embargo, de manera realista, atravesarás ese árbol la mayor parte del tiempo.
Si eso se convierte en un problema, sugeriría usar un mapa de manejador, vinculando la clase concreta del objeto a un cierre que se encarga del manejo. Elimina la fase de recorrido del árbol en favor de un mapeo directo. Sin embargo, tenga en cuenta que si ha configurado un controlador para C.class, no se reconocerá mi objeto d anterior.
Aquí están mis 2 centavos, espero que ayuden ...
La instancia es muy rápida. Se reduce a un bytecode que se utiliza para la comparación de referencia de clase. Pruebe unos cuantos millones de ejemplares en un bucle y compruébelo usted mismo.
Los compiladores modernos de JVM / JIC han eliminado el impacto en el rendimiento de la mayoría de las operaciones tradicionalmente "lentas", incluyendo el ejemplo, el manejo de excepciones, la reflexión, etc.
Como escribió Donald Knuth, "Debemos olvidarnos de las pequeñas eficiencias, digamos que aproximadamente el 97% de las veces: la optimización prematura es la raíz de todo mal". El rendimiento de instanceof probablemente no será un problema, así que no pierda su tiempo con soluciones exóticas hasta que esté seguro de que ese es el problema.
Los elementos que determinarán el impacto en el rendimiento son:
- El número de clases posibles para las cuales el operador instanceof podría devolver verdadero
- La distribución de sus datos: ¿se resuelven la mayoría de las operaciones de instancia en el primer o segundo intento? Usted querrá poner en primer lugar su probabilidad de devolver operaciones verdaderas.
- El entorno de despliegue. La ejecución en una Sun Solaris VM es significativamente diferente a la JVM de Windows de Sun. Solaris se ejecutará en modo ''servidor'' de forma predeterminada, mientras que Windows se ejecutará en modo cliente. Las optimizaciones JIT en Solaris harán que todos los métodos de acceso puedan ser iguales.
He creado un microbenchmark para cuatro métodos diferentes de envío . 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
Me pondré en contacto con usted en caso de rendimiento. Pero una forma de evitar el problema (o la falta del mismo) en conjunto sería crear una interfaz principal para todas las subclases en las que debe hacer un ejemplo. La interfaz será un superconjunto de todos los métodos en subclases para los que necesita realizar una comprobación de instancias. 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.
Pensé que valdría la pena enviar un contraejemplo al consenso general en esta página de que "instanceof" no es lo suficientemente caro como para preocuparse. Descubrí que tenía un código en un bucle interno que (en algún intento histórico de optimización) tenía
if (!(seq instanceof SingleItem)) {
seq = seq.head();
}
donde llamar a head () en un SingleItem devuelve el valor sin cambios. Sustituyendo el código por
seq = seq.head();
me da una aceleración de 269 ms a 169 ms, a pesar del hecho de que en el bucle suceden algunas cosas bastante pesadas, como la conversión de cadena a doble. Por supuesto, es posible que la aceleración se deba más a la eliminación de la rama condicional que a la eliminación de la instancia del propio operador; Pero pensé que valía la pena mencionarlo.
Respondiendo a tu última pregunta: a menos que un generador de perfiles te lo indique, pasarás una cantidad de tiempo ridícula en una instancia de: Sí, lo estás haciendo.
Antes de preguntarse sobre la optimización de algo que nunca fue necesario optimizar: escriba su algoritmo de la manera más legible y ejecútelo. Ejecútalo hasta que el compilador jit tenga la oportunidad de optimizarlo por sí mismo. Si luego tiene problemas con este código, use un generador de perfiles para decirle dónde puede obtener más y optimizar esto.
En tiempos de compiladores altamente optimizados, es probable que sus suposiciones sobre los cuellos de botella sean completamente erróneas.
Y con el verdadero espíritu de esta respuesta (que creo sinceramente): No sé cómo se relacionan con instanceof y == una vez que el compilador jit tuvo la oportunidad de optimizarla.
Lo olvidé: nunca midas la primera carrera.
Si la velocidad es su único objetivo, usar constantes int para identificar subclases parece reducir milisegundos de 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 aquí es donde está el cuello de botella, tal vez. En mi código, el código de despacho toma el 10% del tiempo total de ejecución y esto puede contribuir a una mejora de la velocidad total del 1%.
También prefiero un enfoque de enumeración, 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;
}
}
}
Te estás enfocando en lo incorrecto. La diferencia entre instanceof y cualquier otro método para verificar lo mismo probablemente ni siquiera sería medible. Si el rendimiento es crítico, Java es probablemente el lenguaje incorrecto. La razón principal es que no puedes controlar cuando la máquina virtual decide que quiere ir a recolectar basura, lo que puede llevar la CPU al 100% durante varios segundos en un programa grande (MagicDraw 10 fue genial para eso). A menos que tenga el control de todas las computadoras, este programa se ejecutará, no podrá garantizar en qué versión de JVM estará instalada, y muchas de las antiguas tuvieron problemas importantes de velocidad. Si se trata de una aplicación pequeña, puede que estés de acuerdo con Java, pero si estás leyendo y descartando datos constantemente , notarás cuando se activa el GC.
Tengo la misma pregunta, pero debido a que no encontré "métricas de rendimiento" para el caso de uso similar al mío, hice un código de ejemplo más. En mi hardware y en 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 un gran número de declaraciones if-else-if, sin embargo, la diferencia será despreciable dentro de 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 muy eficiente, por lo que es poco probable que su rendimiento sufra. Sin embargo, usar muchos ejemplos sugiere un problema de diseño.
Si puedes usar xClass == String.class, esto es más rápido. Nota: no necesitas instanceof para las clases finales.
instanceof
es realmente rápido, tomando solo unas pocas instrucciones de CPU.
Aparentemente, si una clase X
no tiene subclases cargadas (JVM sabe), instanceof
puede optimizarse 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 se encuentren en un lugar compartido, por lo que el costo adicional también es muy bajo
¡Buenas noticias para todos!
InstanceOf es una advertencia de un pobre diseño orientado a objetos.
Las JVM actuales sí significan que el instanceOf no es una preocupación de rendimiento en sí misma. Si se está usando mucho, especialmente para la funcionalidad principal, es probable que sea hora de ver el diseño. El rendimiento (y la simplicidad / capacidad de mantenimiento) de la refactorización para un mejor diseño superará en gran medida a cualquier ciclo real del procesador gastado en la llamada de instanceOf real.
Para dar un ejemplo de programación simplista muy pequeño.
if (SomeObject instanceOf Integer) {
[do something]
}
if (SomeObject instanceOf Double) {
[do something different]
}
Si una arquitectura deficiente, una mejor opción hubiera sido que SomeObject fuera la clase principal de dos clases secundarias, donde cada clase secundaria reemplaza un método (doSomething) para que el código se vea como tal:
Someobject.doSomething();