Et hurtigere alternativ til Java Reflection

I artiklen Specifikationsmønster nævnte jeg for sundheds skyld ikke om en underliggende komponent for pænt at få den ting til at ske. Nu vil jeg uddybe lidt mere omkring JavaBeanUtil-klassen, som jeg fik på plads for at læse værdien for en given fieldNamefra en bestemt javaBeanObject, hvilket i den lejlighed viste sig at være FxTransaction.

Du kan nemt argumentere for, at jeg dybest set kunne have brugt Apache Commons BeanUtils eller et af dets alternativer for at opnå det samme resultat. Men jeg var interesseret i at få mine egne hænder beskidte med noget andet, som jeg vidste ville være langt hurtigere end noget bibliotek bygget oven på den vidt kendte Java Reflection.

Enablereren af ​​den teknik, der bruges til at undgå den meget langsomme refleksion, er invokedynamicinstruktion for bytecode. Kort sagt invokedynamic(eller "indy") var den største ting, der blev introduceret i Java 7 for at bane vejen for implementering af dynamiske sprog oven på JVM gennem dynamisk metodeopkald. Det tillod også senere lambda-ekspression og metodehenvisning i Java 8 såvel som strengkombination i Java 9 at drage fordel af det.

I en nøddeskal udnytter teknikken, jeg er ved at bedre beskrive nedenfor, LambdaMetafactory og MethodHandle for dynamisk at skabe en implementering af Function. Dens enkeltmetode delegerer et opkald til den aktuelle målmetode med en kode defineret inde i lambdakroppen.

Den målmetode, der er tale om her, er den aktuelle getter-metode, der har direkte adgang til det felt, vi vil læse. Jeg skal også sige, at hvis du er fortrolig med de pæne ting, der kom op i Java 8, vil du finde nedenstående kodestykker ret lette at forstå. Ellers kan det være vanskeligt med et øjeblik.

Et kig på den hjemmelavede JavaBeanUtil

Den følgende metode er det værktøj, der bruges til at læse en værdi fra et JavaBean-felt. Det tager JavaBean-objektet og et enkelt fieldAeller endog indlejret felt adskilt af perioder, for eksempelnestedJavaBean.nestedJavaBean.fieldA

For optimal ydeevne cachelagrer jeg den dynamisk oprettede funktion, der er den egentlige måde at læse indholdet af en given på fieldName. Så inde i getCachedFunctionmetoden, som du kan se ovenfor, er der en hurtig sti, der udnytter ClassValue til caching, og der er kun den langsomme createAndCacheFunctionsti, der er udført, hvis intet er cachelagret indtil videre.

Den langsomme sti vil grundlæggende delegere til createFunctionsmetoden, der returnerer en liste over funktioner, der skal reduceres ved at kæde dem ved hjælp af Function::andThen. Når funktioner er lænket, kan du forestille dig en slags indlejrede opkald som getNestedJavaBean().getNestedJavaBean().getFieldA(). Endelig efter kædning sætter vi simpelthen den reducerede funktion i cache-kaldemetoden cacheAndGetFunction.

Når vi borer lidt mere ind i den langsomme sti til oprettelse af funktioner, skal vi navigere individuelt gennem pathfeltvariablen ved at opdele den som nedenfor:

Ovenstående createFunctionsmetode delegerer individet fieldNameog dets klasseholdertype til createFunctionmetode, som finder den nødvendige getter baseret på javaBeanClass.getDeclaredMethods(). Når det er placeret, kortlægges det til et Tuple-objekt (facilitet fra Vavr-bibliotek), der indeholder returtypen for getter-metoden og den dynamisk oprettede funktion, som fungerer som om det var selve getter-metoden.

Denne tuple mapping gøres ved createTupleWithReturnTypeAndGettersammenholdt med createCallSitefremgangsmåde som følger:

I ovenstående to metoder bruger jeg en konstant kaldet LOOKUP, som simpelthen er en henvisning til MethodHandles.Lookup. Med det kan jeg oprette et direkte metodehåndtag baseret på den tidligere lokaliserede getter-metode. Og endelig overføres den oprettede MethodHandle til createCallSitemetode, hvorved lambda-kroppen til funktionen produceres ved hjælp af LambdaMetafactory. Derefter kan vi i sidste ende hente CallSite-forekomsten, som er funktionsindehaveren.

Bemærk, at hvis jeg ville beskæftige mig med settere, kunne jeg bruge en lignende tilgang ved at udnytte BiFunction i stedet for Function.

Benchmark

For at måle gevinsten ved ydeevne brugte jeg den altid fantastiske JMH (Java Microbenchmark Harness), som sandsynligvis vil være en del af JDK 12. Som du måske ved, er resultaterne bundet til platformen, så til reference vil jeg bruge en enkelt 1x6 i5-8600K 3.6GHzog Linux x86_64såvel som Oracle JDK 8u191og GraalVM EE 1.0.0-rc9.

Til sammenligning brugte jeg Apache Commons BeanUtils, et velkendt bibliotek for de fleste Java-udviklere, og et af dets alternativer kaldet Jodd BeanUtil, som hævder at være næsten 20% hurtigere.

Benchmark-scenarie indstilles som følger:

Benchmarket er drevet af, hvor dybt vi skal hente noget værdi i henhold til de fire forskellige niveauer, der er angivet ovenfor. For hver fieldNameudfører JMH 5 iterationer på 3 sekunder hver for at varme tingene op og derefter 5 iterationer på 1 sekund hver for faktisk at måle. Hvert scenarie gentages derefter 3 gange for med rimelighed at samle metrics.

Resultater

Lad os starte med resultaterne samlet fra JDK 8u191løbet:

Det værste scenarie ved hjælp af invokedynamictilgang er meget hurtigere end det hurtigste scenario fra de to andre biblioteker. Det er en enorm forskel, og hvis du er i tvivl om resultaterne, kan du altid downloade kildekoden og lege, som du vil.

Lad os nu se, hvordan det samme benchmark fungerer GraalVM EE 1.0.0-rc9

De fulde resultater kan ses her med den smukke JMH Visualizer.

Bemærkninger

Den enorme forskel er, fordi JIT-compiler kender CallSiteog MethodHandlemeget godt og ved, hvordan de skal placeres ganske godt i modsætning til refleksionsmetoden. Du kan også se, hvor lovende GraalVM er. Dens kompilator gør et virkelig fantastisk stykke arbejde, der er i stand til en fantastisk forbedring af ydeevnen til refleksionsmetoden.

Hvis du er nysgerrig og vil spille videre, opfordrer jeg dig til at hente kildekoden fra min Github. Husk, at jeg ikke opfordrer dig til at lave din egen hjemmelavede JavaBeanUtilog bruge i produktionen. Snarere er mit mål her blot at fremvise mit eksperiment og de muligheder, vi kan få fra invokedynamic.