Desempenho de reflexão de Java

Respostas:

169

Sim absolutamente. Procurar uma classe via reflexão é, por magnitude , mais caro.

Citando a documentação do Java sobre reflexão :

Como a reflexão envolve tipos resolvidos dinamicamente, determinadas otimizações da máquina virtual Java não podem ser executadas. Conseqüentemente, as operações reflexivas têm desempenho mais lento do que as contrapartes não refletivas e devem ser evitadas nas seções de código que são chamadas com freqüência em aplicativos sensíveis ao desempenho.

Aqui está um teste simples que eu fiz em 5 minutos na minha máquina, executando o Sun JRE 6u10:

public class Main {

    public static void main(String[] args) throws Exception
    {
        doRegular();
        doReflection();
    }

    public static void doRegular() throws Exception
    {
        long start = System.currentTimeMillis();
        for (int i=0; i<1000000; i++)
        {
            A a = new A();
            a.doSomeThing();
        }
        System.out.println(System.currentTimeMillis() - start);
    }

    public static void doReflection() throws Exception
    {
        long start = System.currentTimeMillis();
        for (int i=0; i<1000000; i++)
        {
            A a = (A) Class.forName("misc.A").newInstance();
            a.doSomeThing();
        }
        System.out.println(System.currentTimeMillis() - start);
    }
}

Com estes resultados:

35 // no reflection
465 // using reflection

Lembre-se de que a pesquisa e a instanciação são feitas juntas e, em alguns casos, a pesquisa pode ser refatorada, mas este é apenas um exemplo básico.

Mesmo se você apenas instanciar, ainda terá um impacto no desempenho:

30 // no reflection
47 // reflection using one lookup, only instantiating

Mais uma vez, YMMV.

Yuval Adam
fonte
5
Na minha máquina, a chamada .newInstance () com apenas uma chamada Class.forName () possui aproximadamente 30. Dependendo da versão da VM, a diferença pode estar mais próxima do que você pensa com uma estratégia de cache apropriada.
21713 Sean Reilly
56
@ Peter Lawrey abaixo apontou que este teste era completamente inválido porque o compilador estava otimizando a solução não refletiva (pode até provar que nada foi feito e otimizar o loop for). Precisa ser re-trabalhado e provavelmente deve ser removido do SO como informações ruins / enganosas. Coloque em cache os objetos criados em uma matriz nos dois casos para impedir que o otimizador otimize. (Ele não pode fazer isso na situação reflexivo porque não pode provar que o construtor não tem efeitos colaterais)
Bill K
6
@ Bill K - não vamos nos deixar levar. Sim, os números estão desativados devido a otimizações. Não, o teste não é completamente inválido. Eu adicionei uma chamada que remove qualquer possibilidade de distorcer o resultado, e os números ainda estão empilhados contra a reflexão. Em qualquer caso, lembre-se que este é um micro-referência muito bruto que só mostra que a reflexão sempre incorre uma certa sobrecarga
Yuval Adam
4
Esta é provavelmente uma referência inútil. Dependendo do que algo faz. Se não fizer nada com efeito colateral visível, seu benchmark executará apenas código morto.
Ns1983
9
Acabei de testemunhar a JVM otimizando a reflexão 35 vezes. Executar o teste repetidamente em um loop é como testar o código otimizado. Primeira iteração: 3045ms, segunda iteração: 2941ms, terceira iteração: 90ms, quarta iteração: 83ms. Código: c.newInstance (i). c é um construtor. Código não reflexivo: novo A (i), que produz 13, 4, 3 .. ms vezes. Portanto, sim, a reflexão neste caso foi lenta, mas não tão lenta quanto o que as pessoas estão concluindo, porque a cada teste que estou vendo, elas simplesmente executam o teste uma vez sem dar à JVM a oportunidade de substituir códigos de bytes por máquinas código.
6133 Mike
87

Sim, é mais lento.

Mas lembre-se da maldita regra nº 1 - A otimização prematura é a raiz de todo o mal

(Bem, pode ser empatado com # 1 para DRY)

Juro que, se alguém viesse até mim no trabalho e me perguntasse isso, ficaria muito atento ao código deles nos próximos meses.

Você nunca deve otimizar até ter certeza de que precisa, até então, basta escrever um código legível e bom.

Ah, e também não quero escrever código estúpido. Basta pensar na maneira mais limpa possível de fazê-lo - sem copiar e colar, etc. (Ainda tenha cuidado com coisas como loops internos e usando a coleção que melhor se adapta às suas necessidades - Ignorar essas não é uma programação "não otimizada" , é "ruim" de programação)

Me assusta quando ouço perguntas como essa, mas depois esqueço que todo mundo precisa aprender todas as regras antes de realmente entender. Você o receberá depois de passar um mês depurando algo que alguém "Otimizou".

EDITAR:

Uma coisa interessante aconteceu neste tópico. Verifique a resposta nº 1, é um exemplo de quão poderoso o compilador é para otimizar as coisas. O teste é completamente inválido porque a instanciação não reflexiva pode ser completamente fatorada.

Lição? NUNCA otimize até escrever uma solução limpa e bem codificada e provar que é muito lenta.

Bill K
fonte
28
Eu concordo totalmente com o sentimento desta resposta, no entanto, se você está prestes a tomar uma decisão importante de design, ajuda a ter uma idéia sobre desempenho, para que você não siga um caminho totalmente impraticável. Talvez ele esteja apenas fazendo a devida diligência?
Limbic System
26
-1: Evitar fazer as coisas da maneira errada não é otimização, é apenas fazer as coisas. A otimização está fazendo as coisas da maneira errada e complicada, devido a preocupações reais ou imaginárias.
soru
5
@soru concordo totalmente. Escolher uma lista vinculada a uma lista de matriz para uma classificação de inserção é simplesmente a maneira correta de fazer as coisas. Mas essa pergunta em particular - existem bons casos de uso para os dois lados da pergunta original; portanto, escolher um com base no desempenho e não na solução mais utilizável estaria errado. Não tenho certeza se estamos discordando, por isso não sei por que você disse "-1".
Bill K
14
Qualquer programador sensato de analista precisa considerar a eficiência desde o início ou você pode acabar com um sistema que NÃO pode ser otimizado em um prazo eficiente e econômico. Não, você não otimiza todos os ciclos do relógio, mas certamente emprega as melhores práticas para algo tão básico quanto a instanciação de classe. Este exemplo é excelente por que você considera essas questões relacionadas à reflexão. Seria um programador muito ruim que seguisse em frente e usasse a reflexão em um sistema de milhões de linhas apenas para descobrir mais tarde que eram ordens de magnitude muito lentas.
RichieHH
2
@ Richard Riley Geralmente, a instanciação de classe é um evento bastante raro para as classes selecionadas nas quais você usará a reflexão. Suponho que você esteja certo - algumas pessoas podem instanciar todas as classes de maneira reflexiva, mesmo as que são recriadas constantemente. Eu chamaria isso de péssima programação (embora, mesmo assim, você PODE implementar um cache de instâncias de classe para reutilização após o fato e não prejudicar muito o seu código - então eu acho que ainda diria SEMPRE projetar para facilitar a leitura, depois criar um perfil e otimizar mais tarde)
Bill K
36

Você pode achar que A a = new A () está sendo otimizado pela JVM. Se você colocar os objetos em uma matriz, eles não terão um desempenho tão bom. ;) As seguintes impressões ...

new A(), 141 ns
A.class.newInstance(), 266 ns
new A(), 103 ns
A.class.newInstance(), 261 ns

public class Run {
    private static final int RUNS = 3000000;

    public static class A {
    }

    public static void main(String[] args) throws Exception {
        doRegular();
        doReflection();
        doRegular();
        doReflection();
    }

    public static void doRegular() throws Exception {
        A[] as = new A[RUNS];
        long start = System.nanoTime();
        for (int i = 0; i < RUNS; i++) {
            as[i] = new A();
        }
        System.out.printf("new A(), %,d ns%n", (System.nanoTime() - start)/RUNS);
    }

    public static void doReflection() throws Exception {
        A[] as = new A[RUNS];
        long start = System.nanoTime();
        for (int i = 0; i < RUNS; i++) {
            as[i] = A.class.newInstance();
        }
        System.out.printf("A.class.newInstance(), %,d ns%n", (System.nanoTime() - start)/RUNS);
    }
}

Isso sugere que a diferença é de cerca de 150 ns na minha máquina.

Peter Lawrey
fonte
então você acabou de matar o otimizador, agora as duas versões são lentas. A reflexão é, portanto, ainda muito lenta.
22710 gbjbaanb
13
@gbjbaanb Se o otimizador estava otimizando a criação em si, não era um teste válido. @ O teste de Peter é, portanto, válido, porque na verdade compara os tempos de criação (o otimizador não seria capaz de funcionar em QUALQUER situação do mundo real, porque em qualquer situação do mundo real você precisa dos objetos que você está instanciando).
Bill K
10
@ nes1983 Nesse caso, você poderia ter aproveitado a oportunidade para criar uma melhor referência. Talvez você possa oferecer algo construtivo, como o que deveria estar no corpo do método.
Peter Lawrey
1
no meu mac, openjdk 7u4, a diferença é 95ns versus 100ns. Em vez de armazenar A na matriz, eu armazeno hashCodes. Se você disser -verbose: class, poderá ver quando o ponto de acesso gera bytecode para a construção de A e a aceleração que o acompanha.
264 Ron
@ PeterLawrey Se eu procurar uma vez (uma chamada para Class.getDeclaredMethod) e depois ligar Method.invokevárias vezes? Estou usando a reflexão uma ou tantas vezes quanto a invoco? Pergunta de acompanhamento, e se, em vez disso Method, for um Constructore eu fizer Constructor.newInstancevárias vezes?
ATM
28

Se realmente houver necessidade de algo mais rápido que a reflexão e não for apenas uma otimização prematura, a geração de bytecode com ASM ou uma biblioteca de nível superior é uma opção. Gerar o bytecode pela primeira vez é mais lento do que apenas usar reflexão, mas depois que o bytecode foi gerado, ele é tão rápido quanto o código Java normal e será otimizado pelo compilador JIT.

Alguns exemplos de aplicativos que usam geração de código:

  • A chamada de métodos em proxies gerados pelo CGLIB é um pouco mais rápida que os proxies dinâmicos do Java , porque o CGLIB gera bytecode para seus proxies, mas os proxies dinâmicos usam apenas reflexão ( eu medi o CGLIB para ser cerca de 10x mais rápido nas chamadas de método, mas a criação dos proxies era mais lenta).

  • JSerial gera bytecode para ler / gravar os campos de objetos serializados, em vez de usar reflexão. Existem alguns benchmarks no site do JSerial.

  • Não tenho 100% de certeza (e não sinto vontade de ler a fonte agora), mas acho que o Guice gera bytecode para fazer a injeção de dependência. Corrija-me se eu estiver errado.

Esko Luontola
fonte
27

"Significativo" é inteiramente dependente do contexto.

Se você estiver usando a reflexão para criar um único objeto manipulador com base em algum arquivo de configuração e gastando o resto do seu tempo executando consultas no banco de dados, isso é insignificante. Se você estiver criando um grande número de objetos via reflexão em um loop apertado, sim, é significativo.

Em geral, a flexibilidade do projeto (quando necessário!) Deve direcionar o uso da reflexão, não do desempenho. No entanto, para determinar se o desempenho é um problema, é necessário criar um perfil, em vez de obter respostas arbitrárias em um fórum de discussão.

kdgregory
fonte
24

Há alguma sobrecarga na reflexão, mas é muito menor nas VMs modernas do que costumava ser.

Se você estiver usando a reflexão para criar todos os objetos simples do seu programa, algo está errado. Usá-lo ocasionalmente, quando você tiver um bom motivo, não deve ser um problema.

Marcus Downing
fonte
11

Sim, há um impacto no desempenho ao usar o Reflection, mas uma possível solução alternativa para otimização está armazenando em cache o método:

  Method md = null;     // Call while looking up the method at each iteration.
      millis = System.currentTimeMillis( );
      for (idx = 0; idx < CALL_AMOUNT; idx++) {
        md = ri.getClass( ).getMethod("getValue", null);
        md.invoke(ri, null);
      }

      System.out.println("Calling method " + CALL_AMOUNT+ " times reflexively with lookup took " + (System.currentTimeMillis( ) - millis) + " millis");



      // Call using a cache of the method.

      md = ri.getClass( ).getMethod("getValue", null);
      millis = System.currentTimeMillis( );
      for (idx = 0; idx < CALL_AMOUNT; idx++) {
        md.invoke(ri, null);
      }
      System.out.println("Calling method " + CALL_AMOUNT + " times reflexively with cache took " + (System.currentTimeMillis( ) - millis) + " millis");

vai resultar em:

[java] O método de chamada 1000000 vezes reflexivamente com a pesquisa levou 5618 milis

[java] O método de chamada 1000000 vezes reflexivamente com o cache levou 270 milis

mel3kings
fonte
A reutilização do método / construtor é realmente útil e ajuda, mas observe que o teste acima não fornece números significativos devido aos problemas usuais de benchmarking (sem aquecimento, portanto o primeiro loop em particular mede principalmente o tempo de aquecimento da JVM / JIT).
StaxMan
7

A reflexão é lenta, embora a alocação de objetos não seja tão desesperadora quanto outros aspectos da reflexão. Obter desempenho equivalente com instanciação baseada em reflexão exige que você escreva seu código para que o jit possa dizer qual classe está sendo instanciada. Se a identidade da classe não puder ser determinada, o código de alocação não poderá ser incorporado. Pior, a análise de escape falha e o objeto não pode ser alocado por pilha. Se você tiver sorte, a criação de perfil em tempo de execução da JVM poderá ser útil se esse código esquentar e determinar dinamicamente qual classe predomina e otimizar para essa.

Esteja ciente de que as marcas de microbench neste segmento são profundamente defeituosas, portanto, leve-as com um grão de sal. De longe, o menos defeituoso é o de Peter Lawrey: ele executa o aquecimento para obter os métodos executados e (conscientemente) derrota a análise de escape para garantir que as alocações estejam realmente ocorrendo. Mesmo que você tenha seus problemas, no entanto: por exemplo, pode-se esperar que um grande número de armazenamentos de array derrote caches e buffers de armazenamento, portanto, isso acabará sendo principalmente uma referência de memória se suas alocações forem muito rápidas. (Parabéns a Peter por ter chegado à conclusão certa: que a diferença é "150ns" em vez de "2,5x". Suspeito que ele faça esse tipo de coisa como meio de vida.)

Doradus
fonte
7

Curiosamente, a definição de setAccessible (true), que ignora as verificações de segurança, tem uma redução de 20% no custo.

Sem setAccessible (true)

new A(), 70 ns
A.class.newInstance(), 214 ns
new A(), 84 ns
A.class.newInstance(), 229 ns

Com setAccessible (true)

new A(), 69 ns
A.class.newInstance(), 159 ns
new A(), 85 ns
A.class.newInstance(), 171 ns
Mikhail Kraizman
fonte
1
Parece óbvio para mim em princípio. Esses números escalam linearmente ao executar 1000000invocações?
Lukas Eder
Na verdade, setAccessible()pode haver muito mais diferenças em geral, especialmente para métodos com vários argumentos, portanto, sempre deve ser chamado.
21416 StaxMan
6

Sim, é significativamente mais lento. Estávamos executando um código que fazia isso e, embora eu não tenha as métricas disponíveis no momento, o resultado final foi que tivemos que refatorar esse código para não usar reflexão. Se você souber qual é a classe, basta ligar diretamente para o construtor.

Elie
fonte
1
+1 Eu tive uma experiência semelhante. É bom usar a reflexão apenas se for absolutamente necessário.
Ryan Thames
por exemplo, bibliotecas baseadas em AOP precisam de reflexão.
gaurav
4

No doReflection () está a sobrecarga por causa de Class.forName ("misc.A") (que exigiria uma pesquisa de classe, potencialmente varrendo o caminho da classe no sistema de arquivos), em vez do newInstance () chamado na classe. Eu estou querendo saber como seriam as estatísticas se o Class.forName ("misc.A") for feito apenas uma vez fora do loop for, ele realmente não precisa ser feito para todas as chamadas do loop.

tikoo
fonte
1

Sim, sempre será mais lento criar um objeto por reflexão, porque a JVM não pode otimizar o código no tempo de compilação. Veja os tutoriais Sun / Java Reflection para mais detalhes.

Veja este teste simples:

public class TestSpeed {
    public static void main(String[] args) {
        long startTime = System.nanoTime();
        Object instance = new TestSpeed();
        long endTime = System.nanoTime();
        System.out.println(endTime - startTime + "ns");

        startTime = System.nanoTime();
        try {
            Object reflectionInstance = Class.forName("TestSpeed").newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        endTime = System.nanoTime();
        System.out.println(endTime - startTime + "ns");
    }
}
aledbf
fonte
3
Observe que você deve separar a pesquisa ( Class.forName()) da instância (newInstance ()), porque elas variam significativamente em suas características de desempenho e, ocasionalmente, você pode evitar a pesquisa repetida em um sistema bem projetado.
Joachim Sauer
3
Além disso: você precisa executar cada tarefa muitas e muitas vezes para obter uma referência útil: primeiro, todas as ações são muito lentas para serem medidas com segurança e, em segundo lugar, você precisa aquecer a VM do HotSpot para obter números úteis.
Joachim Sauer
1

Freqüentemente, você pode usar o Apache commons BeanUtils ou PropertyUtils cuja introspecção (basicamente eles armazenam em cache os metadados sobre as classes para que eles nem sempre precisem usar a reflexão).

sproketboy
fonte
0

Eu acho que depende de quão leve / pesado o método de destino é. se o método de destino for muito leve (por exemplo, getter / setter), pode ser 1 a 3 vezes mais lento. se o método de destino demorar cerca de 1 milissegundo ou mais, o desempenho será muito próximo. aqui está o teste que fiz com o Java 8 e reflectasm :

public class ReflectionTest extends TestCase {    
    @Test
    public void test_perf() {
        Profiler.run(3, 100000, 3, "m_01 by refelct", () -> Reflection.on(X.class)._new().invoke("m_01")).printResult();    
        Profiler.run(3, 100000, 3, "m_01 direct call", () -> new X().m_01()).printResult();    
        Profiler.run(3, 100000, 3, "m_02 by refelct", () -> Reflection.on(X.class)._new().invoke("m_02")).printResult();    
        Profiler.run(3, 100000, 3, "m_02 direct call", () -> new X().m_02()).printResult();    
        Profiler.run(3, 100000, 3, "m_11 by refelct", () -> Reflection.on(X.class)._new().invoke("m_11")).printResult();    
        Profiler.run(3, 100000, 3, "m_11 direct call", () -> X.m_11()).printResult();    
        Profiler.run(3, 100000, 3, "m_12 by refelct", () -> Reflection.on(X.class)._new().invoke("m_12")).printResult();    
        Profiler.run(3, 100000, 3, "m_12 direct call", () -> X.m_12()).printResult();
    }

    public static class X {
        public long m_01() {
            return m_11();
        }    
        public long m_02() {
            return m_12();
        }    
        public static long m_11() {
            long sum = IntStream.range(0, 10).sum();
            assertEquals(45, sum);
            return sum;
        }    
        public static long m_12() {
            long sum = IntStream.range(0, 10000).sum();
            assertEquals(49995000, sum);
            return sum;
        }
    }
}

O código de teste completo está disponível no GitHub: ReflectionTest.java

user_3380739
fonte