Pregunta

Estoy trabajando en una aplicación y un enfoque de diseño implica un uso extremadamente intensivo de instanceof operador.Si bien sé que el diseño OO generalmente intenta evitar el uso 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 única 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 que pensé en resolverlo fue usar una primitiva entera de "identificación de tipo" y usar una máscara de bits para representar las categorías de las subclases, y luego simplemente hacer una comparación de máscara de bits de las subclases "identificación de tipo" con un máscara constante que representa la categoría.

Es instanceof ¿Optimizado de alguna manera por la JVM para que sea más rápido que eso?Quiero seguir con Java, pero el rendimiento de la aplicación es fundamental.Sería genial si alguien que haya pasado por este camino antes pudiera ofrecer algún consejo.¿Estoy siendo demasiado quisquilloso o me estoy centrando en lo incorrecto para optimizar?

¿Fue útil?

Solución

Los compiladores JVM/JIC modernos han eliminado el impacto en el rendimiento de la mayoría de las operaciones tradicionalmente "lentas", incluidas instancia de, manejo de excepciones, reflexión, etc.

Como escribió Donald Knuth: "Deberíamos olvidarnos de las pequeñas eficiencias, digamos aproximadamente el 97% de las veces:la optimización prematura es la raíz de todos los males." El rendimiento de instancia de probablemente no será un problema, así que no pierda el tiempo ideando soluciones alternativas exóticas hasta que esté seguro de que ese es el problema.

Otros consejos

Acercarse

escribí un programa de referencia para evaluar diferentes implementaciones:

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

solía jmh ejecutar el punto de referencia con 100 llamadas de calentamiento, 1000 iteraciones de medición y con 10 bifurcaciones.Entonces, cada opción se midió 10 000 veces, lo que toma 12:18:57 para ejecutar la prueba completa en mi MacBook Pro con macOS 10.12.4 y Java 1.8.El benchmark mide el tiempo promedio de cada opción.Para más detalles ver mi implementación en GitHub.

En aras de la exhaustividad:Hay un 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.

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

en un bucle de 10.000.000, la instancia de me dio 63-96 ms, y la cadena igual me dio 106-230 ms

Usé java jvm 6.

Entonces, en mi prueba simple, es más rápido hacer una instancia de en lugar de una comparación de cadenas de un carácter.

usar .equals() de Integer en lugar de string me dio el mismo resultado, solo que cuando usé == fui más rápido que instanciaOf en 20 ms (en un bucle de 10.000.000)

Los elementos que determinarán el impacto en el desempeño son:

  1. El número de clases posibles para las cuales el operador instancia de podría devolver verdadero
  2. La distribución de sus datos: ¿la mayoría de las instancias de operaciones se resuelven en el primer o segundo intento?Querrá poner primero las operaciones con mayor probabilidad de devolver operaciones verdaderas.
  3. El entorno de implementación.La ejecución en una máquina virtual Sun Solaris es significativamente diferente a la JVM 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 sean iguales.

Creé un microbenchmark para cuatro métodos diferentes de envío.Los resultados de Solaris son los siguientes, siendo el número menor el más rápido:

InstanceOf 3156
class== 2925 
OO 3083 
Id 3067 

Respondiendo a tu última pregunta:A menos que un perfilador le diga que pasa cantidades ridículas de tiempo en una instancia de:Sí, eres quisquilloso.

Antes de preguntarte acerca de optimizar algo que nunca necesitó ser optimizado:Escriba su algoritmo de la forma más legible y ejecútelo.Ejecútelo hasta que el compilador jit tenga la oportunidad de optimizarlo.Si luego tiene problemas con este fragmento de código, utilice un generador de perfiles que le indique dónde obtener el máximo provecho y optimizarlo.

En tiempos de compiladores altamente optimizados, sus conjeturas sobre los cuellos de botella probablemente serán completamente erróneas.

Y fiel al espíritu de esta respuesta (que creo sinceramente):No sé en absoluto cómo se relacionan instancia de y == una vez que el compilador jit tuvo la oportunidad de optimizarlo.

Me olvidé:Nunca mida la primera carrera.

Tengo la misma pregunta, pero como no encontré 'métricas de rendimiento' para un caso de uso similar al mío, hice más código de muestra.En mi hardware y Java 6 y 7, la diferencia entre instancia de y cambiar en 10 millones de iteraciones es

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

Por lo tanto, instancia de es realmente más lenta, especialmente en una gran cantidad de declaraciones 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 y requiere 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 en el mismo lugar, por lo que el costo adicional también es muy bajo.

¡Buenas noticias para todos!

instancia de probablemente será más costosa que un simple igual en la mayoría de las implementaciones del mundo real (es decir, aquellas en las que instancia de realmente es necesaria y no se puede resolver simplemente anulando un método común, como todos los libros de texto para principiantes, así como Demian sugiere arriba).

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

w extiende x

Un implemento 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 informática (d instancia de x) requiere tomar d.getClass(), recorrer las interfaces que implementa para saber si una es == para x y, si no, hacerlo nuevamente de forma recursiva para todos sus antepasados...En nuestro caso, si realiza una primera exploración en amplitud de ese árbol, obtendrá al menos 8 comparaciones, suponiendo que y y z no extiendan nada...

Es probable que la complejidad de un árbol de derivación del mundo real sea mayor.En algunos casos, el JIT puede optimizar la mayor parte, si es capaz de resolver de antemano que d es, en todos los casos posibles, una instancia de algo que extiende x.Sin embargo, de manera realista, la mayor parte del tiempo atravesará ese árbol.

Si eso se convierte en un problema, sugeriría usar un mapa de controlador, vinculando la clase concreta del objeto a un cierre que realiza el manejo.Elimina la fase de recorrido 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 código de bytes JVM.Debería ser bastante rápido.

No debería decir que si tiene un interruptor en el que está probando si un objeto es una instancia de alguna subclase, es posible que sea necesario reelaborar su diseño.Considere la posibilidad de trasladar el comportamiento específico de la subclase a las propias subclases.

La instancia de es muy rápida.Se reduce a un código de bytes que se utiliza para la comparación de referencias de clases.Pruebe algunos millones de instancias en un bucle y compruébelo usted mismo.

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

Soy un gran admirador de los pequeños objetos de datos que se pueden utilizar de muchas maneras.Si sigue el enfoque de anulación (polimórfico), sus objetos sólo se pueden utilizar "de una manera".

Aquí es donde entran los patrones...

Puede utilizar el envío doble (como en el patrón de visitante) para pedirle a cada objeto que "lo llame" pasando por sí mismo; esto resolverá el tipo de objeto. Sin embargo (nuevamente) necesitarás una clase que pueda "hacer cosas" con todos los subtipos posibles.

Prefiero usar un patrón de estrategia, donde puedes registrar estrategias para cada subtipo que quieras manejar.Algo como lo siguiente.Tenga en cuenta que esto solo ayuda para coincidencias de tipos exactos, pero tiene la ventaja de que es extensible: los contribuyentes 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 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);
    }
}

instanciade es muy eficiente, por lo que es poco probable que su rendimiento se vea afectado.Sin embargo, el uso de muchos casos sugiere un problema de diseño.

Si puedes usar xClass == String.class, esto es más rápido.Nota:No necesitas instancia de para las clases finales.

Es difícil decir cómo una determinada JVM implementa una instancia de, pero en la mayoría de los casos, los objetos son comparables a las estructuras y las clases también lo son y cada estructura de objeto tiene un puntero a la estructura de clase de la que es una instancia.Entonces, en realidad, instancia de 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 exista un compilador JIT y que haga un trabajo decente.

Teniendo en cuenta que esto solo es acceder a un puntero, obtener un puntero en un cierto desplazamiento al que apunta el puntero y compararlo 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 en realidad puede sea ​​muy rápido.

Sin embargo, no es necesario, depende mucho de la JVM.Sin embargo, si esta resulta ser 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 tiene un compilador JIT y solo interpreta código debería poder realizar una prueba de instancia prácticamente en poco tiempo.

En vez de es una advertencia de un diseño orientado a objetos deficiente.

Las JVM actuales significan la en vez de No es una gran preocupación de rendimiento en sí misma.Si lo usa mucho, especialmente para la funcionalidad principal, probablemente sea el momento de mirar el diseño.Las ganancias de rendimiento (y simplicidad/mantenibilidad) de la refactorización para un mejor diseño compensarán con creces cualquier ciclo real de procesador invertido en el proceso real. en vez de llamar.

Para dar un ejemplo de programación muy pequeño y simplista.

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

¿Es una arquitectura deficiente? Una mejor opción hubiera sido que SomeObject fuera la clase principal de dos clases secundarias donde cada clase secundaria anula un método (doSomething) para que el código se vea así:

Someobject.doSomething();

Me comunicaré con usted en caso de desempeño.Pero una forma de evitar problemas (o la falta de ellos) por completo sería crear una interfaz principal para todas las subclases en las que necesita ejecutar instancia de.La interfaz será un súper conjunto de todo los métodos en subclases para los cuales necesita realizar una verificació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 lo solucioné en el pasado.

Generalmente, la razón por la cual el operador "instanceof" está mal visto en un caso como ese (donde elstanceof busca subclases de esta clase base) es porque lo que debería hacer es mover las operaciones a un método y anularlo para el método 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 hacer que la implementación de "doEverything()" en Clase1 llame a "doThis()", y en Clase2 llame a "doThat()", y así sucesivamente.

En la versión moderna de Java, el operador instancia de es más rápido que una simple llamada a un 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 muchas instancias de.Entonces, un modificador que solo llama una vez a getType() es más rápido.

Si la velocidad es su único objetivo, entonces el uso de constantes int para identificar subclases parece reducir 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 OO, pero si su análisis de rendimiento indica que aquí es donde está su cuello de botella, entonces tal vez.En mi código, el código de envío toma el 10 % del tiempo total de ejecución y esto tal vez contribuyó a una mejora total de la velocidad del 1 %.

Escribo una prueba de rendimiento basada en jmh-java-benchmark-archetype:2.21.JDK es openjdk y la versión es 1.8.0_212.La máquina de prueba es mac pro.El resultado de la prueba es:

Benchmark                Mode  Cnt    Score   Error   Units
MyBenchmark.getClasses  thrpt   30  510.818 ± 4.190  ops/us
MyBenchmark.instanceOf  thrpt   30  503.826 ± 5.546  ops/us

El resultado muestra que:getClass es mejor que instanciaOf, lo cual es contrario a otras pruebas.Sin embargo, no sé por qué.

El código de prueba está a continuación:

public class MyBenchmark {

public static final Object a = new LinkedHashMap<String, String>();

@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public boolean instanceOf() {
    return a instanceof Map;
}

@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public boolean getClasses() {
    return a.getClass() == HashMap.class;
}

public static void main(String[] args) throws RunnerException {
    Options opt =
        new OptionsBuilder().include(MyBenchmark.class.getSimpleName()).warmupIterations(20).measurementIterations(30).forks(1).build();
    new Runner(opt).run();
}
}

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

Debería contar más sobre el problema, tal vez podría utilizar una tienda asociativa, p.un Mapa<Clase, Objeto> si solo está interesado en los tipos concretos.

Con respecto a la nota de Peter Lawrey de que no necesitas una instancia de para las clases finales y solo puedes usar una igualdad de referencia, ¡ten cuidado!Aunque las clases finales no se pueden ampliar, no se garantiza que sean cargadas por el mismo cargador de clases.Utilice únicamente x.getClass() == SomeFinal.class o similares 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 de enumeración, pero usaría una clase base abstracta para forzar a las subclases a implementar el getType() método.

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 valdría la pena presentar un contraejemplo al consenso general en esta página de que "instanceof" no es lo suficientemente caro como para preocuparse.Descubrí que tenía algo de código en un bucle interno que (en algún intento histórico de optimización)

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

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

seq = seq.head();

me da una aceleración de 269 ms a 169 ms, a pesar de que suceden algunas cosas bastante pesadas en el bucle, 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 operador en sí;pero pensé que valía la pena mencionarlo.

Te estás concentrando en algo equivocado.La diferencia entre instancia de y cualquier otro método para comprobar lo mismo probablemente ni siquiera sería mensurable.Si el rendimiento es crítico entonces Java probablemente sea el lenguaje equivocado.La razón principal es que no se puede controlar cuándo la VM 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 excelente para eso).A menos que tenga el control de todas las computadoras en las que se ejecutará este programa, no puede garantizar en qué versión de JVM estará, y muchas de las más antiguas tenían problemas importantes de velocidad.Si es una aplicación pequeña, puede que estés bien con Java, pero si estás constantemente leyendo y descartando datos, entonces voluntad Observe cuando el GC entra en acción.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top