Ātrāka alternatīva Java Reflection

Rakstā Specifikācijas paraugs saprāta labad es neminēju par pamatkomponentu, lai šī lieta labi notiktu. Tagad es nedaudz sīkāk izklāstīšu JavaBeanUtil klasi, kuru es izveidoju, lai nolasītu dotā vērtību fieldNameno konkrētā javaBeanObject, kas šajā gadījumā izrādījās FxTransaction.

Jūs varat viegli apgalvot, ka, lai sasniegtu to pašu rezultātu, es būtībā varētu izmantot Apache Commons BeanUtils vai kādu no tā alternatīvām. Bet mani interesēja sasmērēt savas rokas ar kaut ko citu, par ko es zināju, ka tas būs daudz ātrāk nekā jebkura bibliotēka, kas būvēta virs plaši pazīstamās Java refleksijas.

Metodes, ko izmanto, lai izvairītos no ļoti lēnas refleksijas, iespējotājs ir invokedynamicbaitkoda instrukcija. Īsi sakot, invokedynamic(vai “indy”) bija vislielākā lieta, kas ieviesta Java 7, lai pavērtu ceļu dinamisku valodu ieviešanai virs JVM, izmantojot dinamisku metožu izsaukšanu. Vēlāk tas arī ļāva no tā gūt labumu lambda izteiksmei un metodei Java 8, kā arī virkņu savienošanai Java 9.

Īsāk sakot, tehnika, kuru es labāk aprakstīšu zemāk, izmanto LambdaMetafactory un MethodHandle, lai dinamiski izveidotu Funkcijas ieviešanu. Tās vienotā metode deleģē izsaukumu uz faktisko mērķa metodi ar kodu, kas definēts lambda korpusa iekšpusē.

Mērķa metode šeit ir faktiskā getter metode, kurai ir tieša piekļuve laukam, kuru mēs vēlamies lasīt. Turklāt man jāsaka, ka, ja jūs esat diezgan labi iepazinies ar jaukajām lietām, kas radās Java 8, zemāk redzamie koda fragmenti būs diezgan viegli saprotami. Pretējā gadījumā tas var būt grūts no pirmā acu uzmetiena.

Palūrēt uz pašu gatavotu JavaBeanUtil

Šī metode ir lietderība, ko izmanto, lai nolasītu vērtību no JavaBean lauka. Tam nepieciešams JavaBean objekts un viens fieldAvai pat ligzdots lauks, atdalīts ar punktiem, piemēram,nestedJavaBean.nestedJavaBean.fieldA

Lai sasniegtu optimālu veiktspēju, es kešoju dinamiski izveidoto funkciju, kas ir faktiskais dotā satura lasīšanas veids fieldName. Tātad getCachedFunction, kā redzat iepriekš, metodes iekšpusē ir ātrs ceļš, kas kešatmiņai izmanto ClassValue, un lēnais createAndCacheFunctionceļš tiek izpildīts tikai tad, ja līdz šim nekas nav saglabāts kešatmiņā.

Lēnais ceļš būtībā deleģēs createFunctionsmetodi, kas atgriež samazināmo funkciju sarakstu, izmantojot ķēdes Function::andThen. Kad funkcijas ir saistītas ar ķēdi, varat iedomāties kaut kādus ligzdotus zvanus, piemēram getNestedJavaBean().getNestedJavaBean().getFieldA(),. Visbeidzot, pēc ķēdes mēs vienkārši ievietojam samazināto funkciju kešatmiņas izsaukšanas cacheAndGetFunctionmetodē.

Nedaudz vairāk iedziļinoties lēnajā funkciju izveidošanas ceļā, mums atsevišķi jāpārvietojas pa lauka pathmainīgo, sadalot to, kā norādīts zemāk:

Iepriekš minētā createFunctionsmetode deleģē indivīdu fieldNameun tā klases turētāja tipu createFunctionmetodei, kas, pamatojoties uz to, atradīs nepieciešamo getter javaBeanClass.getDeclaredMethods(). Kad tas ir atrasts, tas tiek piesaistīts Tuple objektam (iekārta no Vavr bibliotēkas), kas satur getter metodes atgriešanās veidu un dinamiski izveidoto funkciju, kurā darbosies tā, it kā tā būtu pati faktiskā getter metode.

Šī dubultā kartēšana tiek veikta createTupleWithReturnTypeAndGetterkopā ar createCallSitemetodi šādi:

Iepriekš minētajās divās metodēs es izmantoju konstantu, ko sauc LOOKUP, kas ir vienkārši atsauce uz MethodHandles.Lookup. Ar to es varu izveidot tiešas metodes rokturi, pamatojoties uz iepriekš atrasto getter metodi. Un visbeidzot, izveidotais MethodHandle tiek nodots createCallSitemetodei, ar kuras funkciju lambda ķermenis tiek ražots, izmantojot LambdaMetafactory. No turienes galu galā mēs varam iegūt CallSite instanci, kas ir funkciju turētājs.

Ievērojiet, ka, ja es vēlētos nodarboties ar seteriem, es varētu izmantot līdzīgu pieeju, izmantojot Function, nevis BiFunction.

Etalons

Lai novērtētu veiktspējas pieaugumu, es izmantoju vienmēr lielisko JMH (Java Microbenchmark Harness), kas, visticamāk, būs daļa no JDK 12. Kā jūs, iespējams, zināt, rezultāti ir saistīti ar platformu, tāpēc uzziņai es izmantojot vienu 1x6 i5-8600K 3.6GHzun Linux x86_64kā arī Oracle JDK 8u191un GraalVM EE 1.0.0-rc9.

Salīdzinājumam es izmantoju Apache Commons BeanUtils, plaši pazīstamu bibliotēku lielākajai daļai Java izstrādātāju, un vienu no tās alternatīvām sauc Jodd BeanUtil, kas apgalvo, ka tā ir gandrīz par 20% ātrāka.

Etalona scenārijs ir noteikts šādi:

Etalonu nosaka tas, cik dziļi mēs iegūsim kādu vērtību atbilstoši iepriekš norādītajiem četriem dažādiem līmeņiem. Katram fieldNameJMH veiks 5 atkārtojumus pa 3 sekundēm, lai sasildītu lietas, un pēc tam 5 atkārtojumus pa 1 sekundei, lai faktiski izmērītu. Pēc tam katrs scenārijs atkārtosies 3 reizes, lai pamatoti apkopotu metriku.

Rezultāti

Sāksim ar JDK 8u191skrējiena rezultātiem:

Sliktākais scenārijs, izmantojot invokedynamicpieeju, ir daudz ātrāks nekā ātrākais scenārijs no pārējām divām bibliotēkām. Tā ir milzīga atšķirība, un, ja jūs šaubāties par rezultātiem, jūs vienmēr varat lejupielādēt pirmkodu un spēlēt, kā vēlaties.

Tagad redzēsim, kā darbojas tas pats etalons GraalVM EE 1.0.0-rc9

Visus rezultātus var apskatīt šeit, izmantojot jauko JMH Visualizer.

Novērojumi

Milzīgā atšķirība ir tāda, ka JIT sastādītājs zina CallSiteun MethodHandleļoti labi, un zina, kā tos diezgan labi iekļaut, pretstatā refleksijas pieejai. Jūs varat arī redzēt, cik daudzsološs ir GraalVM. Tās sastādītājs veic patiesi lielisku darbu, spējot lieliski uzlabot refleksijas pieeju.

Ja jūs esat ziņkārīgs un vēlaties spēlēt tālāk, es iesaku jums izvilkt pirmkodu no mana Github. Paturiet prātā, ka es jūs nemudinu darīt pašmāju JavaBeanUtilun izmantot ražošanā. Drīzāk mans mērķis šeit ir vienkārši parādīt savu eksperimentu un iespējas, no kurām mēs varam gūt invokedynamic.