Una alternativa más rápida a Java Reflection

En el artículo Patrón de especificación, en aras de la cordura, no mencioné un componente subyacente para hacer que eso suceda. Ahora, elaboraré un poco más sobre la clase JavaBeanUtil, que puse en su lugar para leer el valor de un dado fieldNamede un particular javaBeanObject, que en esa ocasión resultó ser FxTransaction.

Puede argumentar fácilmente que básicamente podría haber usado Apache Commons BeanUtils o una de sus alternativas para lograr el mismo resultado. Pero estaba interesado en ensuciarme las manos con algo diferente que sabía que sería mucho más rápido que cualquier biblioteca construida sobre la conocida Java Reflection.

El habilitador de la técnica utilizada para evitar la reflexión muy lenta es el invokedynamicinstrucción de código de bytes. Brevemente, invokedynamic(o "indy") fue lo mejor introducido en Java 7 para allanar el camino para implementar lenguajes dinámicos en la parte superior de la JVM a través de la invocación de métodos dinámicos. Más tarde, también permitió la expresión lambda y la referencia de método en Java 8, así como la concatenación de cadenas en Java 9 para beneficiarse de ella.

En pocas palabras, la técnica que voy a describir mejor a continuación aprovecha LambdaMetafactory y MethodHandle para crear dinámicamente una implementación de Function. Su método único delega una llamada al método de destino real con un código definido dentro del cuerpo lambda.

El método de destino en cuestión aquí es el método getter real que tiene acceso directo al campo que queremos leer. Además, debo decir que si está bastante familiarizado con las cosas buenas que surgieron dentro de Java 8, encontrará los siguientes fragmentos de código bastante fáciles de entender. De lo contrario, puede resultar complicado de un vistazo.

Un vistazo al JavaBeanUtil casero

El siguiente método es la utilidad utilizada para leer un valor de un campo JavaBean. Toma el objeto JavaBean y un campo único fieldAo incluso anidado separado por puntos, por ejemplo,nestedJavaBean.nestedJavaBean.fieldA

Para un rendimiento óptimo, estoy almacenando en caché la función creada dinámicamente que es la forma real de leer el contenido de un determinado fieldName. Entonces, dentro del getCachedFunctionmétodo, como puede ver arriba, hay una ruta rápida que aprovecha ClassValue para el almacenamiento en caché y está la createAndCacheFunctionruta lenta que se ejecuta solo si no se ha almacenado en caché hasta ahora.

La ruta lenta básicamente delegará al createFunctionsmétodo que está devolviendo una lista de funciones que se reducirán al encadenarlas usando Function::andThen. Cuando las funciones están encadenadas, puede imaginar algún tipo de llamadas anidadas como getNestedJavaBean().getNestedJavaBean().getFieldA(). Finalmente, después de encadenar, simplemente colocamos la función reducida en el cacheAndGetFunctionmétodo de llamada al caché .

Profundizando un poco más en la ruta lenta de la creación de funciones, necesitamos navegar individualmente a través de la pathvariable de campo dividiéndola como se muestra a continuación:

El createFunctionsmétodo anterior delega al individuo fieldNamey su tipo de titular de clase al createFunctionmétodo, que ubicará el getter necesario en función de él javaBeanClass.getDeclaredMethods(). Una vez que se encuentra, se asigna a un objeto Tuple (instalación de la biblioteca Vavr), que contiene el tipo de retorno del método getter y la función creada dinámicamente en la que actuará como si fuera el método getter real en sí.

Este mapeo de tuplas se realiza createTupleWithReturnTypeAndGetterjunto con el createCallSitemétodo siguiente:

En los dos métodos anteriores, utilizo una constante llamada LOOKUP, que es simplemente una referencia a MethodHandles.Lookup. Con eso, puedo crear un identificador de método directo basado en el método getter ubicado anteriormente. Y finalmente, el MethodHandle creado se pasa al createCallSitemétodo mediante el cual el cuerpo lambda para la función se produce utilizando LambdaMetafactory. A partir de ahí, en última instancia, podemos obtener la instancia de CallSite, que es el titular de la función.

Tenga en cuenta que si quisiera tratar con los establecedores, podría usar un enfoque similar al aprovechar BiFunction en lugar de Function.

Punto de referencia

Para medir las ganancias de rendimiento, utilicé el siempre asombroso JMH (Java Microbenchmark Harness), que probablemente formará parte del JDK 12. Como sabrá, los resultados están vinculados a la plataforma, así que como referencia estar utilizando un solo 1x6 i5-8600K 3.6GHzy Linux x86_64así como Oracle JDK 8u191y GraalVM EE 1.0.0-rc9.

A modo de comparación, utilicé Apache Commons BeanUtils, una biblioteca muy conocida para la mayoría de los desarrolladores de Java, y una de sus alternativas llamada Jodd BeanUtil, que afirma ser casi un 20% más rápida.

El escenario de referencia se establece de la siguiente manera:

El punto de referencia está impulsado por la profundidad con la que vamos a recuperar algún valor según los cuatro niveles diferentes especificados anteriormente. Para cada uno fieldName, JMH realizará 5 iteraciones de 3 segundos cada una para calentar las cosas y luego 5 iteraciones de 1 segundo cada una para medir realmente. Cada escenario se repetirá 3 veces para recopilar razonablemente las métricas.

Resultados

Comencemos con los resultados recopilados de la JDK 8u191ejecución:

El peor escenario que utiliza el invokedynamicenfoque es mucho más rápido que el escenario más rápido de las otras dos bibliotecas. Esa es una gran diferencia, y si está dudando de los resultados, siempre puede descargar el código fuente y jugar como quiera.

Ahora, veamos cómo funciona el mismo punto de referencia con GraalVM EE 1.0.0-rc9

Los resultados completos se pueden ver aquí con el agradable visualizador JMH.

Observaciones

La gran diferencia es porque el compilador JIT sabe CallSitey sabe MethodHandlemuy bien cómo integrarlos bastante bien en comparación con el enfoque de reflexión. Además, puede ver lo prometedor que es GraalVM. Su compilador hace un trabajo realmente asombroso al ser capaz de una gran mejora del rendimiento para el enfoque de reflexión.

Si tienes curiosidad y quieres seguir jugando, te animo a que extraigas el código fuente de mi Github. Tenga en cuenta que no le estoy animando a que haga su propio producto casero JavaBeanUtily lo utilice en la producción. Más bien, mi objetivo aquí es simplemente mostrar mi experimento y las posibilidades que podemos obtener invokedynamic.