Rendimiento de reflexión de Java

Respuestas:

174

Si, absolutamente. Buscar una clase a través de la reflexión es, por magnitud , más caro.

Citando la documentación de Java sobre la reflexión :

Debido a que la reflexión involucra tipos que se resuelven dinámicamente, no se pueden realizar ciertas optimizaciones de máquinas virtuales Java. En consecuencia, las operaciones reflectantes tienen un rendimiento más lento que sus contrapartes no reflectantes y deben evitarse en secciones de código que se llaman con frecuencia en aplicaciones sensibles al rendimiento.

Aquí hay una prueba simple que pirateé en 5 minutos en mi máquina, ejecutando 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);
    }
}

Con estos resultados:

35 // no reflection
465 // using reflection

Tenga en cuenta que la búsqueda y la creación de instancias se realizan juntas y, en algunos casos, la búsqueda se puede refactorizar, pero este es solo un ejemplo básico.

Incluso si solo crea una instancia, aún obtiene un impacto en el rendimiento:

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

Nuevamente, YMMV.

Yuval Adam
fuente
5
En mi máquina, la llamada .newInstance () con solo una llamada Class.forName () puntúa 30 más o menos. Dependiendo de la versión de VM, la diferencia puede ser más cercana de lo que cree con una estrategia de almacenamiento en caché adecuada.
Sean Reilly
56
@Peter Lawrey a continuación señaló que esta prueba era completamente inválida porque el compilador estaba optimizando la solución no reflectante (incluso puede probar que no se hace nada y optimizar el bucle for). Necesita ser reelaborado y probablemente debería eliminarse del SO como información errónea / engañosa. Guarde en caché los objetos creados en una matriz en ambos casos para evitar que el optimizador los optimice. (No puede hacer esto en la situación de reflexión porque no puede probar que el constructor no tiene efectos secundarios)
Bill K
6
@Bill K: no nos dejemos llevar. Sí, los números están mal debido a las optimizaciones. No, la prueba no es completamente inválida. Agregué una llamada que elimina cualquier posibilidad de sesgar el resultado, y los números aún se apilan contra la reflexión. En cualquier caso, recuerde que este es un micro-benchmark muy crudo que solo muestra que la reflexión siempre incurre en una cierta sobrecarga
Yuval Adam
4
Este es probablemente un punto de referencia inútil. Dependiendo de lo que haga DoSomething. Si no hace nada con efectos secundarios visibles, entonces su punto de referencia solo ejecuta código inactivo.
nes1983
9
Acabo de presenciar la JVM optimizando la reflexión 35 veces. Ejecutar la prueba repetidamente en un bucle es la forma en que prueba el código optimizado. Primera iteración: 3045ms, segunda iteración: 2941ms, tercera iteración: 90ms, cuarta iteración: 83ms. Código: c.newInstance (i). c es un constructor. Código no reflectante: nuevo A (i), que rinde 13, 4, 3 .. ms tiempos. Entonces, sí, la reflexión en este caso fue lenta, pero no tanto más lenta como lo que la gente está concluyendo, porque cada prueba que veo, simplemente están ejecutando la prueba una vez sin darle a la JVM la oportunidad de reemplazar los códigos de bytes con la máquina. código.
Mike
89

Sí, es más lento.

Pero recuerde la maldita regla número uno: la optimización prematura es la raíz de todo mal

(Bueno, puede estar empatado con el # 1 para DRY)

Lo juro, si alguien se me acercara en el trabajo y me preguntara esto, estaría muy atento a su código durante los próximos meses.

Nunca debe optimizar hasta que esté seguro de que lo necesita, hasta entonces, simplemente escriba un código bueno y legible.

Ah, y tampoco me refiero a escribir código estúpido. Solo piense en la forma más limpia en que puede hacerlo: sin copiar y pegar, etc. (aún desconfíe de cosas como los bucles internos y use la colección que mejor se adapte a sus necesidades: ignorar estos no es una programación "no optimizada" , es una "mala" programación)

Me asusta cuando escucho preguntas como esta, pero luego olvido que todos tienen que aprender todas las reglas por sí mismos antes de entenderlas. Lo obtendrá después de haber pasado un mes-hombre depurando algo que alguien "Optimizó".

EDITAR:

Algo interesante sucedió en este hilo. Compruebe la respuesta n. ° 1, es un ejemplo de lo poderoso que es el compilador para optimizar las cosas. La prueba es completamente inválida porque la instanciación no reflectante puede descartarse por completo.

¿Lección? No optimice NUNCA hasta que haya escrito una solución limpia y codificada y haya demostrado que es demasiado lenta.

Bill K
fuente
29
Estoy totalmente de acuerdo con el sentimiento de esta respuesta, sin embargo, si está a punto de embarcarse en una decisión de diseño importante, es útil tener una idea sobre el rendimiento para no tomar un camino totalmente inviable. ¿Quizás solo está haciendo la debida diligencia?
Sistema límbico
27
-1: Evitar hacer las cosas de manera incorrecta no es optimización, es solo hacer cosas. La optimización es hacer las cosas de forma incorrecta y complicada debido a problemas de rendimiento reales o imaginarios.
soru
5
@soru totalmente de acuerdo. Elegir una lista vinculada en lugar de una lista de matriz para una ordenación por inserción es simplemente la forma correcta de hacer las cosas. Pero esta pregunta en particular: hay buenos casos de uso para ambos lados de la pregunta original, por lo que elegir uno basado en el rendimiento en lugar de la solución más utilizable sería incorrecto. No estoy seguro de que estemos en desacuerdo, así que no estoy seguro de por qué dijo "-1".
Bill K
14
Cualquier programador analista sensato debe considerar la eficiencia en una etapa temprana o podría terminar con un sistema que NO se puede optimizar en un marco de tiempo eficiente y rentable. No, no optimizas todos los ciclos de reloj, pero ciertamente SÍ empleas las mejores prácticas para algo tan básico como la instanciación de clases. Este ejemplo es uno de los motivos por los que considera tales preguntas con respecto a la reflexión. Sería un programador bastante pobre que siguió adelante y usó la reflexión en un sistema de un millón de líneas solo para descubrir más tarde que era demasiado lento en órdenes de magnitud.
RichieHH
2
@Richard Riley Generalmente, la instanciación de clases es un evento bastante raro para las clases seleccionadas en las que usará la reflexión. Sin embargo, supongo que tienes razón: algunas personas pueden crear una instancia de cada clase de manera reflexiva, incluso las que se recrean constantemente. Yo llamaría a eso una programación bastante mala (aunque incluso entonces PODRÍA implementar un caché de instancias de clase para su reutilización después del hecho y no dañar demasiado su código, así que supongo que todavía diría SIEMPRE diseñar para legibilidad, luego perfilar y optimizar más tarde)
Bill K
36

Puede encontrar que A a = new A () está siendo optimizado por la JVM. Si coloca los objetos en una matriz, no funcionan tan bien. ;) Las siguientes impresiones ...

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

Esto sugiere que la diferencia es de aproximadamente 150 ns en mi máquina.

Peter Lawrey
fuente
por lo que acaba de eliminar el optimizador, por lo que ahora ambas versiones son lentas. La reflexión, por tanto, sigue siendo muy lenta.
gbjbaanb
14
@gbjbaanb si el optimizador estaba optimizando la creación en sí, entonces no era una prueba válida. Por lo tanto, la prueba de @ Peter es válida porque en realidad compara los tiempos de creación (el optimizador no podría funcionar en NINGUNA situación del mundo real porque en cualquier situación del mundo real necesita los objetos que está instanciando).
Bill K
10
@ nes1983 En cuyo caso, podría haber aprovechado la oportunidad para crear un mejor punto de referencia. Quizás puedas ofrecer algo constructivo, como lo que debería estar en el cuerpo del método.
Peter Lawrey
1
en mi mac, openjdk 7u4, la diferencia es 95ns versus 100ns. En lugar de almacenar A en la matriz, almaceno hashCodes. Si dice -verbose: class, puede ver cuando el punto de acceso genera un código de bytes para construir A y la aceleración que lo acompaña.
Ron
@PeterLawrey ¿Si busco una vez (una llamada a Class.getDeclaredMethod) y luego llamo Method.invokevarias veces? ¿Estoy usando la reflexión una o tantas veces como la invoco? Pregunta de seguimiento, ¿qué Methodpasa si en lugar de hacerlo es un Constructory lo hago Constructor.newInstancevarias veces?
tmj
29

Si realmente se necesita algo más rápido que la reflexión, y no se trata solo de una optimización prematura, entonces la generación de código de bytes con ASM o una biblioteca de nivel superior es una opción. Generar el código de bytes por primera vez es más lento que simplemente usar la reflexión, pero una vez que se ha generado el código de bytes, es tan rápido como el código Java normal y será optimizado por el compilador JIT.

Algunos ejemplos de aplicaciones que utilizan generación de código:

  • La invocación de métodos de proxies generados por CGLIB es ligeramente más rápido que el de Java proxies dinámicos , porque CGLIB genera el código de bytes de sus aliados, pero proxies dinámicos utilice sólo la reflexión ( Medí CGLIB en alrededor de 10 veces más rápido en las llamadas de método, pero la creación de las proxies era más lento).

  • JSerial genera código de bytes para leer / escribir los campos de objetos serializados, en lugar de utilizar la reflexión. Hay algunos puntos de referencia en el sitio de JSerial.

  • No estoy 100% seguro (y no tengo ganas de leer la fuente ahora), pero creo que Guice genera un código de bytes para hacer una inyección de dependencia. Corrígeme si me equivoco.

Esko Luontola
fuente
27

"Significativo" depende completamente del contexto.

Si está utilizando la reflexión para crear un único objeto de controlador basado en algún archivo de configuración y luego pasa el resto de su tiempo ejecutando consultas de base de datos, entonces es insignificante. Si está creando una gran cantidad de objetos mediante la reflexión en un bucle cerrado, entonces sí, es significativo.

En general, la flexibilidad del diseño (¡donde sea necesario!) Debería impulsar el uso de la reflexión, no el rendimiento. Sin embargo, para determinar si el rendimiento es un problema, debe crear un perfil en lugar de obtener respuestas arbitrarias de un foro de discusión.

kdgregory
fuente
24

Hay algo de sobrecarga con la reflexión, pero es mucho más pequeña en las máquinas virtuales modernas de lo que solía ser.

Si está utilizando la reflexión para crear cada objeto simple en su programa, entonces algo está mal. Usarlo ocasionalmente, cuando tenga una buena razón, no debería ser un problema en absoluto.

Marcus Downing
fuente
11

Sí, hay un impacto en el rendimiento al usar Reflection, pero una posible solución para la optimización es almacenar en caché el 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");

resultará en:

[java] El método de llamada 1000000 veces de forma reflexiva con búsqueda tomó 5618 milis

[java] El método de llamada 1000000 veces de forma refleja con caché tomó 270 milis

mel3kings
fuente
Reutilizar el método / constructor es realmente útil y ayuda, pero tenga en cuenta que la prueba anterior no da números significativos debido a los problemas habituales de evaluación comparativa (no hay calentamiento, por lo que el primer ciclo en particular mide principalmente el tiempo de calentamiento JVM / JIT).
StaxMan
8

Curiosamente, configurar setAccessible (verdadero), que omite los controles de seguridad, tiene una reducción del 20% en el costo.

Sin setAccessible (verdadero)

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

Con setAccessible (verdadero)

new A(), 69 ns
A.class.newInstance(), 159 ns
new A(), 85 ns
A.class.newInstance(), 171 ns
Mikhail Kraizman
fuente
1
Me parece obvio en principio. ¿Estos números se escalan linealmente al ejecutar 1000000invocaciones?
Lukas Eder
En realidad, setAccessible()puede tener mucha más diferencia en general, especialmente para métodos con múltiples argumentos, por lo que siempre se debe llamar.
StaxMan
7

La reflexión es lenta, aunque la asignación de objetos no es tan desesperada como otros aspectos de la reflexión. Lograr un rendimiento equivalente con la creación de instancias basada en reflexión requiere que escriba su código para que el jit pueda decir qué clase se está instanciando. Si no se puede determinar la identidad de la clase, el código de asignación no se puede insertar. Peor aún, el análisis de escape falla y el objeto no se puede asignar en la pila. Si tiene suerte, la generación de perfiles en tiempo de ejecución de la JVM puede acudir al rescate si este código se pone caliente, y puede determinar dinámicamente qué clase predomina y puede optimizar para esa.

Tenga en cuenta que los microbenchmarks en este hilo tienen fallas profundas, así que tómelos con un grano de sal. El menos defectuoso, con mucho, es el de Peter Lawrey: hace carreras de calentamiento para poner los métodos en funcionamiento, y (conscientemente) anula el análisis de escape para garantizar que las asignaciones estén realmente ocurriendo. Sin embargo, incluso ese tiene sus problemas: por ejemplo, se puede esperar que la tremenda cantidad de almacenes de arreglos derrote los cachés y los búferes de almacenamiento, por lo que esto terminará siendo principalmente un punto de referencia de memoria si sus asignaciones son muy rápidas. (Sin embargo, felicitaciones a Peter por haber llegado a la conclusión correcta: que la diferencia es "150ns" en lugar de "2.5x". Sospecho que hace este tipo de cosas para ganarse la vida).

Doradus
fuente
6

Sí, es significativamente más lento. Estábamos ejecutando un código que hacía eso, y aunque no tengo las métricas disponibles en este momento, el resultado final fue que tuvimos que refactorizar ese código para no usar la reflexión. Si sabe cuál es la clase, simplemente llame al constructor directamente.

Elie
fuente
1
+1 Tuve una experiencia similar. Es bueno asegurarse de usar la reflexión solo si es absolutamente necesario.
Ryan Thames
por ejemplo, las bibliotecas basadas en AOP necesitan reflexión.
Gaurav
4

En doReflection () está la sobrecarga debido a Class.forName ("misc.A") (que requeriría una búsqueda de clase, posiblemente escaneando la ruta de clase en el sistema de archivos), en lugar de la newInstance () llamada en la clase. Me pregunto cómo se verían las estadísticas si Class.forName ("misc.A") se realiza solo una vez fuera del bucle for, realmente no tiene que hacerse para cada invocación del bucle.

tikoo
fuente
1

Sí, siempre será más lento crear un objeto por reflexión porque la JVM no puede optimizar el código en tiempo de compilación. Consulte los tutoriales de Sun / Java Reflection para obtener más detalles.

Vea esta sencilla prueba:

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
fuente
3
Tenga en cuenta que debe separar la búsqueda ( Class.forName()) de la instanciación (nuevaInstancia ()), porque varían significativamente en sus características de rendimiento y ocasionalmente puede evitar la búsqueda repetida en un sistema bien diseñado.
Joachim Sauer
3
Además, debe ejecutar cada tarea muchas, muchas veces para obtener un punto de referencia útil: en primer lugar, las acciones son demasiado lentas para ser medidas de manera confiable y, en segundo lugar, deberá calentar la máquina virtual HotSpot para obtener números útiles.
Joachim Sauer
1

A menudo, puede usar Apache commons BeanUtils o PropertyUtils que hacen introspección (básicamente almacenan en caché los metadatos sobre las clases para que no siempre necesiten usar la reflexión).

sproketboy
fuente
0

Creo que depende de qué tan ligero / pesado sea el método objetivo. si el método objetivo es muy ligero (por ejemplo, captador / definidor), podría ser 1 ~ 3 veces más lento. si el método de destino tarda aproximadamente 1 milisegundo o más, el rendimiento será muy similar. aquí está la prueba que hice con Java 8 y 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;
        }
    }
}

El código de prueba completo está disponible en GitHub: ReflectionTest.java

user_3380739
fuente