A Java Reflection gyorsabb alternatívája

A Specifikációs minta című cikkben az ésszerűség kedvéért nem említettem egy mögöttes komponenst, hogy szépen megvalósítsam ezt a dolgot. Most majd dolgoznia egy kicsit körül JavaBeanUtil osztály, hogy én életbe olvasni az értéket egy adott fieldNameegy adott javaBeanObject, amely ez alkalommal kiderült, hogy FxTransaction.

Könnyen vitathatja, hogy alapvetően az Apache Commons BeanUtils programot vagy annak egyik alternatíváját használhattam volna ugyanazon eredmény elérése érdekében. De érdekelt, hogy bepiszkítsam a kezemet valami mással, amiről tudtam, hogy sokkal gyorsabb lesz, mint bármelyik könyvtár, amely a széles körben ismert Java Reflection tetejére épül.

A nagyon lassú reflexió elkerülése érdekében alkalmazott technika lehetővé teszi a invokedynamicbytecode utasítás. Röviden: invokedynamic(vagy „indy”) volt a legnagyobb dolog, amelyet a Java 7-ben vezettek be annak érdekében, hogy dinamikus módszerek meghívásával előkészítsék az utat a dinamikus nyelvek bevezetésére a JVM tetején. Később lehetővé tette a lambda kifejezés és módszer referencia használatát a Java 8-ban, valamint a string összefűzését a Java 9-ben is.

Dióhéjban, az a módszer, amelyet az alábbiakban jobban leírok, kihasználja a LambdaMetafactory és a MethodHandle eszközöket annak érdekében, hogy dinamikusan létrehozhassam a Function megvalósítását. Egyetlen módszere a lambda törzs belsejében definiált kóddal delegálja a hívást a tényleges cél módszerre.

A szóban forgó célmódszer az a tényleges getter módszer, amely közvetlen hozzáféréssel rendelkezik az olvasni kívánt mezőhöz. Ezenkívül azt kell mondanom, hogy ha Ön ismeri a Java 8-ban felmerült szép dolgokat, akkor az alábbi kódrészleteket meglehetősen könnyen meg fogja találni. Ellenkező esetben egy pillantásra trükkös lehet.

Bepillantás a házi JavaBeanUtil programba

A következő módszer segítségével értéket olvashatunk le egy JavaBean mezőből. Ehhez a JavaBean objektumra és egy fieldAvagy akár egymásba ágyazott mezőre van szükség, pontokkal elválasztva, példáulnestedJavaBean.nestedJavaBean.fieldA

Az optimális teljesítmény érdekében tárolom a dinamikusan létrehozott funkciót, amely az adott tartalom tényleges olvasási módja fieldName. Tehát a getCachedFunctionmódszer belsejében , amint az a fentiekben is látható, van egy gyors út, amely kihasználja a ClassValue értéket a gyorsítótárazáshoz, és ott van a lassú createAndCacheFunctionút, amelyet csak akkor hajtanak végre, ha eddig semmi sem került gyorsítótárba.

A lassú út alapvetően azt a createFunctionsmódszert fogja delegálni , amely visszaadja a csökkenteni kívánt függvények listáját azáltal, hogy láncolja őket Function::andThen. Ha a függvények láncolva vannak, elképzelhető valamilyen beágyazott hívás, például getNestedJavaBean().getNestedJavaBean().getFieldA(). Végül a láncolás után egyszerűen a csökkentett függvényt tesszük a gyorsítótár hívás cacheAndGetFunctionmódszerébe.

Kicsit jobban belemerülve a funkciók létrehozásának lassú útjába, egyedileg kell navigálnunk a mező pathváltozóban az alábbiak szerint felosztva:

A fenti createFunctionsmódszer az egyént fieldNameés az osztálytulajdonos típusát delegálja a createFunctionmódszerre, amely alapján megtalálja a szükséges gettert javaBeanClass.getDeclaredMethods(). Miután megtalálta, egy Tuple objektumra (a Vavr könyvtár létesítményéből) térképezi fel, amely tartalmazza a getter metódus visszatérési típusát és a dinamikusan létrehozott függvényt, amelyben úgy fog működni, mintha maga a tényleges getter módszer lenne.

Ez a kettős leképezés a következő módszerrel createTupleWithReturnTypeAndGetteregyütt történik createCallSite:

A fenti két módszerben egy állandó nevű állandót használok LOOKUP, ami egyszerűen a MethodHandles.Lookup hivatkozás. Ezzel létrehozhatok egy közvetlen metódus fogantyút a korábban elhelyezett getter módszer alapján. Végül a létrehozott MethodHandle-t átadják annak a createCallSitemódszernek, amelyben a funkció lambda testét a LambdaMetafactory segítségével állítják elő. Végül megszerezhetjük a CallSite példányt, amely a funkciótulajdonos.

Ne feledje, hogy ha beállítókkal akarok foglalkozni, hasonló megközelítést alkalmazhatok a BiFunction használatával a Function helyett.

Viszonyítási alap

A teljesítménynövekedés mérése érdekében a mindig félelmetes JMH-t (Java Microbenchmark Harness) használtam, amely valószínűleg a JDK 12 része lesz. Mint azt Ön is tudja, az eredmények a platformhoz vannak kötve, ezért referenciaként egyetlen felhasználásával 1x6 i5-8600K 3.6GHzés Linux x86_64valamint a Oracle JDK 8u191és GraalVM EE 1.0.0-rc9.

Összehasonlításképpen az Apache Commons BeanUtils programot használtam, amely a legtöbb Java-fejlesztő számára jól ismert könyvtár, és annak egyik alternatíváját, a Jodd BeanUtil nevet viseli, amely csaknem 20% -kal gyorsabb.

A referencia-forgatókönyv a következő:

A benchmarkot az vezérli, hogy milyen mélyen fogunk valamilyen értéket beolvasni a fent meghatározott négy különböző szint szerint. Mindegyik fieldNameesetében a JMH 5 ismétlést hajt végre 3 másodpercenként a dolgok felmelegedése érdekében, majd 5 darab 1 másodperces ismétlést a tényleges méréshez. Ezután minden forgatókönyv háromszor megismétlődik, ésszerűen összegyűjtve a mutatókat.

Eredmények

Kezdjük a JDK 8u191futásból összegyűjtött eredményekkel :

A legrosszabb forgatókönyv invokedynamicsokkal gyorsabb, mint a másik két könyvtár leggyorsabb forgatókönyve. Ez óriási különbség, és ha kételkedik az eredményekben, akkor bármikor letöltheti a forráskódot, és tetszés szerint játszhat.

Most nézzük meg, hogy ugyanaz a referenciaérték hogyan teljesít GraalVM EE 1.0.0-rc9

A teljes eredmények itt tekinthetők meg a szép JMH Visualizer segítségével.

Megfigyelések

Az óriási különbség azért van, mert a JIT-fordító ismeri CallSiteés MethodHandlenagyon jól tudja, és tudja, hogyan lehet ezeket nagyon jól beilleszteni, szemben a reflexiós megközelítéssel. Láthatja, mennyire ígéretes a GraalVM. A fordítója valóban fantasztikus munkát végez, és nagyszerű teljesítménynövelésre képes a reflexiós megközelítéshez.

Ha kíváncsi vagy, és tovább akarsz játszani, arra kérlek, hogy vedd le a forráskódot a Github-ból. Ne feledje, hogy nem arra biztatlak, hogy készítsen saját házi készítésű terméket, JavaBeanUtilés használja fel a termelésben. Inkább az a célom, hogy egyszerűen bemutassam a kísérletemet és a lehetőségeket, amelyekből ki tudunk szerezni invokedynamic.