O impacto no desempenho do uso de instanceof em Java

Eu estou trabalhando em uma aplicação e uma abordagem de design envolve o uso extremamente pesado do operador instanceof . Embora eu saiba que o design OO geralmente tenta evitar o uso de instanceof , essa é uma história diferente e essa questão está puramente relacionada ao desempenho. Eu queria saber se há algum impacto no desempenho? É tão rápido quanto == ?

Por exemplo, eu tenho uma class base com 10 subclasss. Em uma única function que usa a class base, eu verifico se a class é uma instância da subclass e realizo alguma rotina.

Uma das outras maneiras que pensei em resolvê-lo foi usar uma primitiva de inteiro “id de tipo”, e usar uma máscara de bits para representar categorias de subclasss, e então fazer uma comparação de máscara de bits das subclasss “id de tipo” máscara constante representando a categoria.

A instanceof alguma forma é otimizada pela JVM para ser mais rápida que isso? Eu quero ficar com Java, mas o desempenho do aplicativo é fundamental. Seria legal se alguém que já esteve nessa estrada antes pudesse dar alguns conselhos. Estou criticando demais ou me concentrando na coisa errada para otimizar?

    Os compiladores JVM / JIC modernos removeram o desempenho da maioria das operações tradicionalmente “lentas”, incluindo instanceof, exception handling, reflection, etc.

    Como Donald Knuth escreveu: “Devemos esquecer pequenas eficiências, digamos 97% do tempo: a otimização prematura é a raiz de todo o mal”. O desempenho do instanceof provavelmente não será um problema, portanto, não perca tempo com soluções exóticas até ter certeza de que esse é o problema.

    Abordagem

    Eu escrevi um programa de benchmark para avaliar diferentes implementações:

    1. instanceof implementação (como referência)
    2. object orientado através de uma class abstrata e @Override um método de teste
    3. usando uma implementação de tipo próprio
    4. Implementação getClass() == _.class

    Eu usei o jmh para executar o benchmark com 100 chamadas de aquecimento, 1000 iterações sob medição e 10 garfos. Assim, cada opção foi medida com 10.000 vezes, o que leva 12:18:57 para executar todo o benchmark no meu MacBook Pro com o macOS 10.12.4 e o Java 1.8. O benchmark mede o tempo médio de cada opção. Para mais detalhes, veja minha implementação no GitHub .

    Por uma questão de completude: Existe uma versão anterior desta resposta e meu benchmark .

    Resultados

     |  Operação |  Tempo de execução em nanossegundos por operação |  Relativo ao exemplo de |
     | ------------ | ------------------------------------ - | ------------------------ |
     |  INSTANCEOF |  39,598 ± 0,022 ns / op |  100,00% |
     |  GETCLASS |  39687 ± 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

    No Java 1.8, instanceof é a abordagem mais rápida, embora getClass() seja muito próxima.

    Acabei de fazer um teste simples para ver como o desempenho instanceOf está comparando a uma simples chamada s.equals () para um object de string com apenas uma letra.

    em um loop 10.000.000 o instanceOf me deu 63-96ms, e a string é igual a mim deu 106-230ms

    Eu usei java jvm 6.

    Portanto, no meu teste simples é mais rápido fazer uma instanceOf em vez de uma comparação de uma cadeia de caracteres.

    usando .equals () de Integer em vez de string me deu o mesmo resultado, somente quando eu usei o == i foi mais rápido que instanceOf por 20ms (em um loop 10.000.000)

    Os itens que determinarão o impacto no desempenho são:

    1. O número de classs possíveis para as quais o operador instanceof poderia retornar true
    2. A distribuição dos seus dados – a maioria das instâncias de operações é resolvida na primeira ou segunda tentativa? Você vai querer colocar sua maior probabilidade de retornar as operações verdadeiras primeiro.
    3. O ambiente de implementação A execução em uma VM Sun Solaris é significativamente diferente da Windows JVM da Sun. O Solaris será executado no modo ‘servidor’ por padrão, enquanto o Windows será executado no modo cliente. As otimizações de JIT no Solaris farão com que todo o access a methods seja o mesmo.

    Eu criei um microbenchmark para quatro methods diferentes de envio . Os resultados do Solaris são os seguintes, com o menor número sendo mais rápido:

     InstanceOf 3156 class== 2925 OO 3083 Id 3067 

    Respondendo à sua última pergunta: a menos que um profiler lhe diga que você gasta uma quantidade ridícula de tempo em um instante: Sim, você está com dificuldades.

    Antes de pensar sobre como otimizar algo que nunca precisou ser otimizado: escreva seu algoritmo da maneira mais legível e execute-o. Execute-o, até que o compilador jit tenha a chance de otimizá-lo. Se você tiver problemas com este trecho de código, use um profiler para lhe dizer onde obter mais e otimizar isso.

    Em tempos de compiladores altamente otimistas, seus palpites sobre gargalos provavelmente serão completamente errados.

    E no verdadeiro espírito desta resposta (que eu sinceramente acredito): eu absolutamente não sei como a ocorrência e a relação se dão, uma vez que o compilador tem a chance de otimizá-lo.

    Eu esqueci: nunca meça a primeira corrida.

    Eu tenho a mesma pergunta, mas porque eu não encontrei ‘métricas de desempenho’ para o caso de uso semelhante ao meu, eu fiz mais algumas amostras de código. No meu hardware e Java 6 e 7, a diferença entre a ocorrência e a ativação de 10mln iterações é

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

    Assim, o instanceof é realmente mais lento, especialmente em um número enorme de declarações if-else-if, no entanto a diferença será insignificante dentro da aplicação 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 é realmente rápido, recebendo apenas algumas instruções da CPU.

    Aparentemente, se uma class X não tiver subclasss carregadas (a JVM sabe), o instanceof pode ser otimizado como:

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

    O custo principal é apenas uma leitura!

    Se o X tiver subclasss carregadas, mais algumas leituras serão necessárias; Eles são provavelmente co-localizados de modo que o custo extra também seja muito baixo.

    Boas notícias, pessoal!

    O exemplo provavelmente será mais caro do que um simples igual na maioria das implementações do mundo real (isto é, aquelas onde a instância é realmente necessária, e você não pode simplesmente resolvê-lo substituindo um método comum, como todos os livros didáticos iniciantes assim como Demian acima sugere).

    Por que é que? Porque o que provavelmente vai acontecer é que você tem várias interfaces, que fornecem alguma funcionalidade (digamos, interfaces x, yez), e alguns objects para manipular que podem (ou não) implementar uma dessas interfaces … mas Não diretamente. Diga, por exemplo, eu tenho:

    w estende x

    Um implementa w

    B estende A

    C estende B, implementa y

    D estende C, implementa z

    Suponha que eu esteja processando uma instância de D, o object d. A computação (d instanceof x) requer que o d.getClass () faça um loop pelas interfaces que ele implementa para saber se um é == to x, e se não o fizer novamente recursivamente para todos os seus ancestrais … No nosso caso, se você fizer uma primeira exploração da tree, produz pelo menos 8 comparações, supondo que yez não estenda nada …

    A complexidade de uma tree de derivação do mundo real provavelmente será maior. Em alguns casos, o JIT pode otimizar a maior parte dele, se ele puder resolver antecipadamente como sendo, em todos os casos possíveis, uma instância de algo que estenda x. Realisticamente, no entanto, você vai passar por essa travessia de tree na maior parte do tempo.

    Se isso se tornar um problema, sugiro usar um mapa de manipulador, vinculando a class concreta do object a um fechamento que faz o tratamento. Remove a fase de passagem da tree em favor de um mapeamento direto. No entanto, tenha em atenção que, se tiver definido um manipulador para C.class, o meu object d acima não será reconhecido.

    aqui estão meus 2 centavos, espero que eles ajudem …

    Por exemplo, é muito eficiente, por isso é improvável que o seu desempenho sofra. No entanto, usar muitos exemplos sugere um problema de design.

    Se você pode usar xClass == String.class, isso é mais rápido. Nota: você não precisa de instanceof para as classs finais.

    ‘instanceof’ é na verdade um operador, como + ou -, e acredito que ele tenha sua própria instrução de bytecode da JVM. Deve ser muito rápido.

    Eu não deveria que se você tem um switch onde você está testando se um object é uma instância de alguma subclass, então seu design pode precisar ser retrabalhado. Considere levar o comportamento específico da subclass para as próprias subclasss.

    A instância é muito rápida. Ele resume-se a um bytecode que é usado para comparação de referência de class. Tente alguns milhões de instâncias em um loop e veja por si mesmo.

    É difícil dizer como uma determinada JVM implementa a instância, mas na maioria dos casos, os Objetos são comparáveis ​​a estruturas e classs são tão bem e toda estrutura de object tem um ponteiro para a estrutura de class da qual é uma instância. Então, na verdade, o caso de

     if (o instanceof java.lang.String) 

    pode ser tão rápido quanto o seguinte código C

     if (objectStruct->iAmInstanceOf == &java_lang_String_class) 

    supondo que um compilador JIT esteja no lugar e faça um trabalho decente.

    Considerando que isto está apenas acessando um ponteiro, obtendo um ponteiro em um determinado deslocamento, o ponteiro aponta para e comparando isto com outro ponteiro (que é basicamente o mesmo que testar para números de 32 bits sendo iguais), eu diria que a operação pode realmente seja muito rápido.

    Não precisa, no entanto, depender muito da JVM. No entanto, se essa for a operação de gargalo em seu código, considerarei a implementação da JVM um pouco pobre. Mesmo um que não tenha compilador JIT e só interprete o código deve ser capaz de fazer uma instância de teste em praticamente nenhum momento.

    InstanceOf é um aviso de design orientado a objects pobres.

    JVMs atuais significam que instanceOf não é muito uma preocupação de desempenho em si. Se você está se acostumando muito com isso, especialmente para a funcionalidade principal, provavelmente é hora de olhar para o design. Os ganhos de desempenho (e simplicidade / manutenibilidade) da refatoração para um design melhor superam em muito os ciclos reais de processador gastos na chamada instanceOf real.

    Para dar um exemplo de programação simplista muito pequeno.

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

    É uma arquitetura ruim uma escolha melhor teria sido ter SomeObject ser a class pai de duas classs filho onde cada class filho substitui um método (doSomething) para que o código ficaria assim:

     Someobject.doSomething(); 

    Demian e Paul mencionam um bom ponto; no entanto , a colocação do código para executar realmente depende de como você deseja usar os dados …

    Sou um grande fã de pequenos objects de dados que podem ser usados ​​de várias maneiras. Se você seguir a abordagem de substituição (polimórfica), seus objects só poderão ser usados ​​”one way”.

    É aqui que os padrões entram …

    Você pode usar o despacho duplo (como no padrão de visitante) para pedir que cada object “chame você” passando por si mesmo – isso resolverá o tipo do object. No entanto (novamente) você precisará de uma class que possa “fazer coisas” com todos os possíveis subtipos.

    Eu prefiro usar um padrão de estratégia, onde você pode registrar estratégias para cada subtipo que você deseja manipular. Algo como o seguinte. Observe que isso ajuda apenas em correspondências de tipo exato, mas tem a vantagem de ser extensível – os colaboradores de terceiros podem adicionar seus próprios tipos e manipuladores. (Isso é bom para estruturas dinâmicas como OSGi, onde novos pacotes podem ser adicionados)

    Espero que isso inspire algumas outras ideias …

     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< ? extends SomeCommonSuperType>> handlers_ = new HashMap, Handler< ? extends SomeCommonSuperType>>(); 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); } } 

    Geralmente, a razão pela qual o operador “instanceof” é desaprovado em um caso como esse (onde o instanceof está verificando subclasss dessa class base) é porque o que você deve fazer é mover as operações para um método e substituí-lo pelo apropriado. subclasss. Por exemplo, se você tem:

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

    Você pode replace isso com

     o.doEverything(); 

    e, em seguida, ter a implementação de “doEverything ()” na chamada de Class1 “doThis ()” e na chamada de Class2 “doThat ()” e assim por diante.

    Na versão moderna de Java, o operador instanceof é mais rápido como uma simples chamada de método. Isso significa:

     if(a instanceof AnyObject){ } 

    é mais rápido como:

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

    Outra coisa é se você precisar colocar vários instantes em cascata. Então um switch que só chama uma vez getType () é mais rápido.

    Se a velocidade é o seu único objective, usar constantes do tipo int para identificar subclasss parece reduzir em milissegundos o tempo

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

    OO design terrível, mas se sua análise de desempenho indica que é onde você gargalo é, então, talvez. No meu código, o código de despacho leva 10% do tempo total de execução e isso pode contribuir para uma melhoria de 1% na velocidade total.

    Voltarei a você no exemplo de desempenho. Mas uma maneira de evitar o problema (ou a falta dele) seria criar uma interface pai para todas as subclasss nas quais você precisa fazer o instanceof. A interface será um super conjunto de todos os methods em subclasss para as quais você precisa fazer o instanceof check. Quando um método não se aplica a uma subclass específica, simplesmente forneça uma implementação simulada desse método. Se não entendi mal o problema, foi assim que resolvi o problema no passado.

    Você deve medir / perfilar se realmente é um problema de desempenho em seu projeto. Se for, eu recomendaria um novo design – se possível. Tenho certeza que você não pode superar a implementação nativa da plataforma (escrita em C). Você também deve considerar a inheritance múltipla neste caso.

    Você deve informar mais sobre o problema, talvez você possa usar um repository associativo, por exemplo, um Map se você estiver interessado apenas nos tipos concretos.

    No que diz respeito à nota de Peter Lawrey, você não precisa de exemplo para as aulas finais e pode apenas usar uma igualdade de referência, tenha cuidado! Mesmo que as classs finais não possam ser estendidas, elas não têm garantia de serem carregadas pelo mesmo carregador de class. Apenas use x.getClass () == SomeFinal.class ou sua laia se você estiver absolutamente certo de que há apenas um classloader em jogo para essa seção de código.

    Eu também prefiro uma abordagem enum, mas eu usaria uma class base abstrata para forçar as subclasss a implementar o 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; } } } 

    Eu pensei que poderia valer a pena enviar um contra-exemplo para o consenso geral nesta página que “instanceof” não é caro o suficiente para se preocupar. Eu descobri que eu tinha algum código em um laço interno que (em alguma tentativa histórica de otimização) fiz

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

    onde chamando head () em um SingleItem retorna o valor inalterado. Substituindo o código por

     seq = seq.head(); 

    dá-me uma aceleração de 269ms para 169ms, apesar do fato de que há algumas coisas muito pesadas acontecendo no loop, como conversão de string para duplo. É possível, é claro, que a aceleração seja mais devida à eliminação do ramo condicional do que à eliminação da instância do próprio operador; mas achei que valeria a pena mencionar.

    Você está se concentrando na coisa errada. A diferença entre a instância e qualquer outro método para verificar a mesma coisa provavelmente não seria mensurável. Se o desempenho for crítico, o Java provavelmente é a linguagem errada. A razão principal é que você não pode controlar quando a VM decide que vai coletar lixo, o que pode levar a CPU a 100% por vários segundos em um programa grande (o MagicDraw 10 foi ótimo para isso). A menos que você esteja no controle de todos os computadores em que este programa será executado, você não poderá garantir qual versão da JVM estará ativada, e muitos dos mais antigos tiveram grandes problemas de velocidade. Se for um aplicativo pequeno, você pode ficar bem com o Java, mas se estiver constantemente lendo e descartando dados, perceberá quando o GC entrar em ação.