Eine schnellere Alternative zu Java Reflection

In dem Artikel Spezifikationsmuster habe ich aus Gründen der Vernunft keine zugrunde liegende Komponente erwähnt, um dies zu ermöglichen. Jetzt werde ich etwas näher auf die JavaBeanUtil-Klasse eingehen, die ich eingerichtet habe, um den Wert für eine bestimmte fieldNameaus einer bestimmten Klasse zu lesen javaBeanObject, was sich in diesem Fall als FxTransaction herausstellte.

Sie können leicht argumentieren, dass ich im Grunde Apache Commons BeanUtils oder eine seiner Alternativen hätte verwenden können, um das gleiche Ergebnis zu erzielen. Aber ich war daran interessiert, meine eigenen Hände mit etwas anderem schmutzig zu machen, von dem ich wusste, dass es viel schneller sein würde als jede Bibliothek, die auf der weithin bekannten Java Reflection aufbaut.

Der Wegbereiter für die Technik, mit der die sehr langsame Reflexion vermieden wird, ist die invokedynamicBytecode-Anweisung. Kurz gesagt, invokedynamic(oder „Indy“) war das Beste, was in Java 7 eingeführt wurde, um den Weg für die Implementierung dynamischer Sprachen über die JVM durch den Aufruf dynamischer Methoden zu ebnen. Später konnten auch Lambda-Ausdrücke und Methodenreferenzen in Java 8 sowie die Verkettung von Zeichenfolgen in Java 9 davon profitieren.

Kurz gesagt, die Technik, die ich im Folgenden besser beschreiben werde, nutzt LambdaMetafactory und MethodHandle, um dynamisch eine Implementierung von Function zu erstellen. Die einzelne Methode delegiert einen Aufruf an die eigentliche Zielmethode mit einem Code, der innerhalb des Lambda-Körpers definiert ist.

Die hier fragliche Zielmethode ist die eigentliche Getter-Methode, die direkten Zugriff auf das Feld hat, das wir lesen möchten. Außerdem sollte ich sagen, wenn Sie mit den netten Dingen, die in Java 8 aufgetaucht sind, vertraut sind, werden Sie die folgenden Codefragmente ziemlich leicht verstehen. Andernfalls kann es auf einen Blick schwierig sein.

Ein Blick auf das hausgemachte JavaBeanUtil

Die folgende Methode ist das Dienstprogramm zum Lesen eines Werts aus einem JavaBean-Feld. Es werden das JavaBean-Objekt und ein einzelnes fieldAoder sogar verschachteltes Feld verwendet, die durch Punkte getrennt sind, z.nestedJavaBean.nestedJavaBean.fieldA

Für eine optimale Leistung speichere ich die dynamisch erstellte Funktion zwischen, mit der der Inhalt einer bestimmten Funktion tatsächlich gelesen wird fieldName. getCachedFunctionWie Sie oben sehen können, gibt es innerhalb der Methode einen schnellen Pfad, der den ClassValue für das Caching nutzt, und den langsamen createAndCacheFunctionPfad, der nur ausgeführt wird, wenn bisher nichts zwischengespeichert wurde.

Der langsame Pfad wird grundsätzlich an die createFunctionsMethode delegiert, die eine Liste von Funktionen zurückgibt, die durch Verketten mit reduziert werden sollen Function::andThen. Wenn Funktionen verkettet sind, können Sie sich verschachtelte Aufrufe wie vorstellen getNestedJavaBean().getNestedJavaBean().getFieldA(). Schließlich setzen wir nach der Verkettung einfach die reduzierte Funktion in die Cache-Aufrufmethode cacheAndGetFunction.

Um den langsamen Pfad der Funktionserstellung etwas genauer zu betrachten, müssen wir individuell durch die Feldvariable navigieren, indem wir pathsie wie folgt aufteilen:

Die obige createFunctionsMethode delegiert die Person fieldNameund ihren Klasseninhabertyp an die createFunctionMethode, anhand derer der benötigte Getter basierend gefunden wird javaBeanClass.getDeclaredMethods(). Sobald es gefunden wurde, wird es einem Tuple-Objekt (Einrichtung aus der Vavr-Bibliothek) zugeordnet, das den Rückgabetyp der Getter-Methode und die dynamisch erstellte Funktion enthält, in der es sich so verhält, als wäre es die eigentliche Getter-Methode.

Diese Tupelzuordnung erfolgt createTupleWithReturnTypeAndGetterin Verbindung mit der folgenden createCallSiteMethode:

In den beiden oben genannten Methoden verwende ich eine Konstante namens LOOKUP, die lediglich auf MethodHandles.Lookup verweist. Damit kann ich ein direktes Methodenhandle erstellen, das auf der zuvor lokalisierten Getter-Methode basiert. Und schließlich wird das erstellte MethodHandle an die createCallSiteMethode übergeben, wobei der Lambda-Körper für die Funktion unter Verwendung der LambdaMetafactory erzeugt wird. Von dort können wir letztendlich die CallSite-Instanz erhalten, die der Funktionsinhaber ist.

Beachten Sie, dass ich, wenn ich mich mit Setzern befassen wollte, einen ähnlichen Ansatz verwenden könnte, indem ich BiFunction anstelle von Function nutze.

Benchmark

Um die Leistungssteigerungen zu messen, habe ich das beeindruckende JMH (Java Microbenchmark Harness) verwendet, das wahrscheinlich Teil des JDK 12 sein wird. Wie Sie vielleicht wissen, sind die Ergebnisse an die Plattform gebunden eine einzelne verwenden 1x6 i5-8600K 3.6GHzund Linux x86_64sowie Oracle JDK 8u191und GraalVM EE 1.0.0-rc9.

Zum Vergleich habe ich Apache Commons BeanUtils verwendet, eine für die meisten Java-Entwickler bekannte Bibliothek, und eine ihrer Alternativen namens Jodd BeanUtil, die behauptet, fast 20% schneller zu sein.

Das Benchmark-Szenario wird wie folgt festgelegt:

Der Benchmark hängt davon ab, wie tief wir einen Wert gemäß den vier oben angegebenen Ebenen abrufen werden. Für jede fieldNameführt JMH 5 Iterationen von jeweils 3 Sekunden durch, um die Dinge aufzuwärmen, und dann 5 Iterationen von jeweils 1 Sekunde, um tatsächlich zu messen. Jedes Szenario wird dann dreimal wiederholt, um die Metriken angemessen zu erfassen.

Ergebnisse

Beginnen wir mit den Ergebnissen des JDK 8u191Laufs:

Das schlechteste Szenario mit invokedynamicAnsatz ist viel schneller als das schnellste Szenario aus den beiden anderen Bibliotheken. Das ist ein großer Unterschied, und wenn Sie an den Ergebnissen zweifeln, können Sie jederzeit den Quellcode herunterladen und herumspielen, wie Sie möchten.

Lassen Sie uns nun sehen, wie sich derselbe Benchmark verhält GraalVM EE 1.0.0-rc9

Die vollständigen Ergebnisse können hier mit dem netten JMH Visualizer angezeigt werden.

Beobachtungen

Der große Unterschied ist , weil JIT-Compiler weiß CallSiteund MethodHandlesehr gut und weiß , wie sie ganz gut inline in Bezug auf die Reflexion Ansatz gegenüber . Sie können auch sehen, wie vielversprechend GraalVM ist. Sein Compiler leistet wirklich großartige Arbeit und kann eine hervorragende Leistungssteigerung für den Reflection-Ansatz erzielen.

Wenn Sie neugierig sind und weiter spielen möchten, empfehle ich Ihnen, den Quellcode von meinem Github zu ziehen. Denken Sie daran, ich ermutige Sie nicht, Ihre eigenen hausgemachten JavaBeanUtilund in der Produktion zu verwenden. Mein Ziel hier ist es vielmehr, einfach mein Experiment und die Möglichkeiten, die wir daraus ziehen können, vorzustellen invokedynamic.