Uma alternativa mais rápida ao Java Reflection

Carlos Raphael Blocked Desbloquear Seguir Seguindo 11 de dezembro de 2018

No artigo Specification Pattern , por uma questão de sanidade, eu não mencionei sobre um componente subjacente para fazer isso acontecer. Agora, vou elaborar um pouco mais sobre a classe JavaBeanUtil , que eu coloquei no local para ler o valor de um determinado fieldName de um determinado javaBeanObject , que naquela ocasião acabou sendo FxTransaction .

Você pode facilmente argumentar que eu poderia ter usado basicamente o Apache Commons BeanUtils ou uma de suas alternativas para obter o mesmo resultado. Mas eu estava interessado em sujar minhas próprias mãos com algo diferente que eu sabia que seria muito mais rápido do que qualquer biblioteca construída sobre o amplamente conhecido Java Reflection .

O facilitador da técnica usada para evitar a reflexão muito lenta é a invokedynamic instrução bytecode. Resumidamente, invokedynamic (ou “indy”) foi a maior coisa introduzida no Java 7 para preparar o caminho para implementar linguagens dinâmicas no topo da JVM através da chamada de método dinâmico. Ele também permitiu posteriormente a expressão lambda e a referência de método no Java 8, bem como a concatenação de strings no Java 9 para se beneficiar dele.

Em suma, a técnica que estou prestes a descrever melhor aproveita o LambdaMetafactory e o MethodHandle para criar dinamicamente uma implementação de Function . Seu método único delega uma chamada para o método de destino real com um código definido dentro do corpo lambda.

O método de destino em questão aqui é o método getter real que tem acesso direto ao campo que queremos ler. Além disso, devo dizer que, se você estiver familiarizado com as coisas boas que surgiram no Java 8, você encontrará os trechos de código abaixo bastante fáceis de entender. Caso contrário, pode ser complicado de relance.

Uma espiada no caseiro JavaBeanUtil

O método a seguir é o utilitário usado para ler um valor de um campo JavaBean. Ele toma o objeto JavaBean e um único fieldA ou até mesmo um campo aninhado, separados por pontos, por exemplo, nestedJavaBean.nestedJavaBean.fieldA

Para otimizar o desempenho, estou armazenando em cache a função criada dinamicamente que é a maneira real de ler o conteúdo de um determinado fieldName . Então, dentro do método getCachedFunction , como você pode ver acima, há um caminho rápido usando o ClassValue para armazenamento em cache e o caminho lento createAndCacheFunction executado somente se nada foi armazenado em cache até o momento.

O caminho lento basicamente delegará ao método createFunctions que está retornando uma lista de funções a serem reduzidas encadeando-as usando Function::andThen . Quando as funções são encadeadas, você pode imaginar algum tipo de chamada aninhada como getNestedJavaBean().getNestedJavaBean().getFieldA() . Finalmente, após o encadeamento, simplesmente colocamos a função reduzida no método cacheAndGetFunction cache.

Perfurando um pouco mais no caminho lento da criação da função, precisamos navegar individualmente pela variável do path do campo, dividindo-a conforme abaixo:

O método createFunctions acima delega o individual fieldName e seu tipo de suporte de classe ao método createFunction , que localizará o getter necessário com base em javaBeanClass.getDeclaredMethods() . Uma vez localizado, mapeia para um objeto Tuple (recurso da biblioteca Vavr ), que contém o tipo de retorno do método getter e a função criada dinamicamente na qual atuará como se fosse o próprio método getter.

Esse mapeamento de tupla é feito por createTupleWithReturnTypeAndGetter em conjunto com o método createCallSite seguinte maneira:

Nos dois métodos acima, faço uso de uma constante chamada LOOKUP , que é simplesmente uma referência a MethodHandles.Lookup . Com isso, posso criar um identificador de método direto com base no método getter localizado anteriormente. E finalmente, o MethodHandle criado é passado para o método createCallSite onde o corpo lambda para a função é produzido usando o LambdaMetafactory . De lá, finalmente, podemos obter a instância do CallSite , que é o detentor da função.

Observe que, se eu quisesse lidar com setters, poderia usar uma abordagem semelhante, aproveitando BiFunction em vez de Function .

Referência

Para medir os ganhos de desempenho, usei o sempre incrível JMH ( Java Microbenchmark Harness ), que provavelmente fará parte do JDK 12 . Como você deve saber, os resultados estão vinculados à plataforma, portanto, para referência, utilizarei uma única 1x6 i5-8600K 3.6GHz e Linux x86_64 bem como o Oracle JDK 8u191 e o GraalVM EE 1.0.0-rc9 .

Para comparação, usei o Apache Commons BeanUtils , uma biblioteca bem conhecida para a maioria dos desenvolvedores de Java, e uma de suas alternativas chamada Jodd BeanUtil, que afirma ser quase 20% mais rápida .

Cenário de benchmark é definido da seguinte forma:

O benchmark é impulsionado pela profundidade em que recuperaremos algum valor de acordo com os quatro níveis diferentes especificados acima. Para cada fieldName , o JMH executará 5 iterações de 3 segundos cada para aquecer as coisas e depois 5 iterações de 1 segundo cada para realmente medir. Cada cenário será repetido 3 vezes para reunir as métricas.

Resultados

Vamos começar com os resultados coletados da execução do JDK 8u191 :

Oracle JUK 8u191

O pior cenário usando a abordagem invokedynamic é muito mais rápido que o cenário mais rápido das outras duas bibliotecas. Isso é uma grande diferença, e se você está duvidando dos resultados, você sempre pode baixar o código-fonte e brincar como quiser.

Agora, vamos ver como o mesmo benchmark funciona com o GraalVM EE 1.0.0-rc9

GraalVM EE 1.0.0-rc9

Resultados completos podem ser vistos aqui com o agradável JMH Visualizer.

Observações

A grande diferença é que o compilador JIT conhece muito bem o CallSite e o MethodHandle e sabe como incorporá-los muito bem em oposição à abordagem de reflexão. Além disso, você pode ver como o GraalVM é promissor. Seu compilador faz um trabalho realmente incrível sendo capaz de melhorar o desempenho da abordagem de reflexão.

Se você está curioso e quer continuar jogando, eu sugiro que você puxe o código-fonte do meu Github . Tenha em mente que não estou incentivando você a fazer seu próprio JavaBeanUtil caseiro e usá-lo na produção. O meu objetivo aqui é simplesmente mostrar minha experiência e as possibilidades que podemos obter a partir da invokedynamic .

Texto original em inglês.