Sikertelen cikk a reflexió felgyorsításáról.

Azonnal elmagyarázom a cikk címét. Eredetileg jó, megbízható tanácsot akartam adni a reflexió használatának felgyorsítására egy egyszerű, de reális példán keresztül, de a benchmarking során kiderült, hogy a reflexió nem olyan lassú, mint gondoltam, a LINQ pedig lassabb, mint álmaimban. A végén még egy hibát is vétettem a mérésekben... Ennek a történetnek a részletei a kattintás után és a megjegyzésekben. Mivel a példa elég hétköznapi és alapvetően úgy lett megvalósítva, ahogy a vállalatokban szokás, számomra elég érdekesnek tűnik a bemutató: a cikk fő téma sebességére gyakorolt hatás nem volt észlelhető a külső logika miatt: Moq, Autofac, EF Core és más „körítés” miatt.

Az inspirációmat egy cikk hatására kezdtem el: Miért lassú a reflexió

Mint látható, a szerző javasolja a lefordított delegátok használatát a reflexiós típusok módszereivel való közvetlen hívás helyett, mint a program gyorsításának remek módját. Természetesen van még IL kibocsátás is, de azt el szeretném kerülni, mivel ez a legmunkaigényesebb módja a feladat elvégzésének, ami sok hibát rejt magában.

Figyelembe véve, hogy mindig is hasonló véleményen voltam a reflexió sebességéről, nem szándékozom megkérdőjelezni a szerző következtetéseit.

Gyakran találkozom a reflexió naiv használatával a vállalati környezetben. Egy típust veszünk. Eszköz információt szerzünk. Meghívjuk a SetValue metódust, és mindenki boldog. Az érték eljutott a célmezőbe, mindenki elégedett. Az emberek nem buták — szeniorok és csapatvezetők — a saját kiterjesztéseiket írják az object-re, egy ilyen naiv megvalósítás alapján, „univerzális” mapper-eket egy típusból a másikba. A lényeg általában az: minden mezőt és minden tulajdonságot veszünk, iterálunk rajtuk: ha a tagok nevei megegyeznek, végrehajtjuk a SetValue-t. Időnként kivételeket fogunk el ott, ahol nem találtunk egy tulajdonságot az egyik típusnál, de itt is van megoldás, ami javítja a teljesítményt: Try/catch.

Láttam, hogy az emberek újra feltalálják a parsereket és a mappereket, anélkül, hogy teljes körű információval rendelkeznének arról, hogyan működik a már régebben kitalált kerék. Láttam, hogy az emberek ügyes megvalósításaikat elrejtik stratégiák, interfészek vagy injekciók mögé, mintha ez mentegethetné a későbbi káoszt. Az ilyen megvalósításoktól elfordultam. Valójában a valódi teljesítménycsökkenést soha nem mértem, és ha lehetőségem volt rá, egyszerűen kicseréltem a megvalósítást egy „optimálisabbra”, ha éppen úgy alakult. Így az első mérések, amelyekről alább lesz szó, igazán megleptek.

Azt hiszem, sokan közületek, amikor Richter-t vagy más ideológusokat olvasnak, találkoztak azzal a teljesen jogos állítással, hogy a kódon belüli reflexió rendkívül negatívan hat az alkalmazás teljesítményére.

A reflexió meghívása arra kényszeríti a CLR-t, hogy bejárja a gyűjteményeket a keresett metadadatok után kutatva, betölti azokat és feldolgozza stb. Ezen kívül a reflexió a sorrend bejárása során nagy mennyiségű memória allokálását eredményezi. A memóriafogyasztás közben a CLR megmozdítja a GC-t, és elkezdődnek a fagyások. Ennek lassúnak kellene lennie, hidd el. A modern produkciós szerverek vagy felhőgépek óriási memória mennyisége sem ment meg a magas feldolgozási késleltetésektől. Valójában, minél több memória áll rendelkezésre, annál valószínűbb, hogy ÉSZRE FOGOD VENNI, hogyan működik a GC. A reflexió elvileg egy felesleges piros rongy számára.

Ennek ellenére mindannyian használunk IoC konténereket és adatmappereket, amelyek működési elve szintén a reflexión alapul, azonban ezek teljesítményének általában nincsenek kérdései. Nem, nem azért, mert a függőségek bevezetése és a külső, korlátozott kontextusból való elvonás olyan szükséges dolgok, hogy a teljesítményt bármilyen esetben fel kell áldoznunk. Minden sokkal egyszerűbb – ezek valóban nem gyakorolnak jelentős hatást a teljesítményre.

Az a helyzet, hogy a legelterjedtebb keretrendszerek, amelyek a reflexió technológiáján alapulnak, különféle fortélyokat használnak a hatékonyabb működés érdekében. Általában cache-t használnak. Általában – ezek kifejezések és a kifejezésfán átkonvertált delegáltak. A legjobb automapper egy versenyképes szótárral rendelkezik, amely párosítja a típusokat azzal a funkcióval, amelyik már anélkül tudja átkonvertálni őket, hogy szükség lenne a reflexió meghívására.

Hogyan érhető el ez? Lényegében nem különbözik attól a logikától, amelyet maga a platform használ a JIT kódgenerálásához. Az első metódushíváskor az lefordításra kerül (és igen, ez a folyamat nem gyors), a következő hívásoknál viszont a vezérlés már a lefordított metódushoz kerül, és itt nem lesz jelentős teljesítménycsökkenés.

A mi esetünkben is alkalmazhatjuk a JIT fordítást, és ezután a lefordított viselkedést azonos teljesítménnyel használhatjuk, mint az AOT analógjait. Ebből a szempontból a kifejezések segítségünkre lesznek.

Röviden összefoglalva, a tárgyalt elv a következőképpen fogalmazható meg:
A reflektálás végső eredményét érdemes delegátusként tárolni, amely tartalmazza a lefordított funkciót. Minden szükséges objektumot a típusokról szintén érdemes egy, a típusaid, azaz a munkavállalók által tárolt mezőn kívül cache-elni.

Van ebben logika. Az egészséges ész azt mondja, hogy ha valami lefordítható és cache-elhető, akkor azt érdemes megtenni.

Előrehaladva érdemes megemlíteni, hogy a reflektálás során alkalmazott cache-nek vannak előnyei, még ha nem is használjuk az előzőekben javasolt kifejezés-fordítási módszert. Ezt a cikk szerzőjének tézisét csak megismétlem, amelyre fentebb hivatkoztam.

Most pedig a kódra. Nézzünk meg egy példát, amely a legutóbbi problémán alapul, amellyel egy komoly hitelintézet komoly termelésében kellett szembenéznem. Minden entitás fiktív, hogy senki ne találhassa ki.

Van egy entitás. Hívjuk Contact-nak. Vannak olyan levelek, amelyek standardizált tartalommal rendelkeznek, amelyekből a parser és a hidratáló létrehozza ezeket a kontakteket. Megérkezik egy levél, elolvassuk, kulcs-érték párokra bontjuk, létrehozzuk a kontaktot, és mentjük az adatbázisba.

Ez elemi dolog. Tegyük fel, hogy a kontakt rendelkezik néhány tulajdonsággal: Név, Kor és elérhetőség. Ezeket az adatokat a levélben adjuk át. A vállalkozás azt is szeretné, ha a támogató csapatok gyorsan hozzá tudnának adni új kulcsokat az entitás tulajdonságainak párokra történő térképezéséhez a levél testében. Abban az esetben, ha valaki hibázott a sablonban, vagy ha a kiadás előtt sürgősen el kell indítani a térképezést egy új partnerről, alkalmazkodva az új formátumhoz. Ekkor az új térképezési korrelációt olcsó adatfixként tudjuk hozzáadni. Tehát, ez egy valós példa.

Tesztelünk, létrehozunk teszteket. Működik.

A kódot nem fogom bemutatni: sok forrás keletkezett, és elérhetők a GitHubon a cikk végén található linken. Letöltheti őket, teljesen átkonfigurálhatja, és lemérheti, hogy ez hogyan hatna az ön esetére. Csak két sablonmódszer kódját mutatom be, amelyek megkülönböztetik a gyors hidratort a lassú hidratortól.

A logika a következő: a sablonmódszer párt kap, amelyeket a parser alaplogikája alakít. A LINQ szintje a parser és a hidratáló alaplogikája, amely lekérdezést végez az adatbázis kontextusán, és párokat rendel össze a parser kulcsaival (az ilyen funkciókhoz van kód LINQ nélkül összehasonlításra). Ezután a párok átkerülnek a fő hidratáló módszerbe, és az értékek beállítása történik az entitás megfelelő tulajdonságaiba.

A "Gyors" (Fast előtag a benchmarkokban):

 protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
        {
            var contact = new Contact();
            foreach (var setterMapItem in _proprtySettersMap)
            {
                var correlation = correlations.FirstOrDefault(x => x.PropertyName == setterMapItem.Key);
                setterMapItem.Value(contact, correlation?.Value);
            }
            return contact;
        }

Ahogy látjuk, statikus gyűjteményt használunk a tulajdonságok setterjeivel – ezek kompilált lambda kifejezések, amelyek az entitás setterét hívják. A következő kóddal hozzák létre:

        static FastContactHydrator()
        {
            var type = typeof(Contact);
            foreach (var property in type.GetProperties())
            {
                _proprtySettersMap[property.Name] = GetSetterAction(property);
            }
        }

        private static Action GetSetterAction(PropertyInfo property)
        {
            var setterInfo = property.GetSetMethod();
            var paramValueOriginal = Expression.Parameter(property.PropertyType, "value");
            var paramEntity = Expression.Parameter(typeof(Contact), "entity");
            var setterExp = Expression.Call(paramEntity, setterInfo, paramValueOriginal).Reduce();
            
            var lambda = (Expression<Action>)Expression.Lambda(setterExp, paramEntity, paramValueOriginal);

            return lambda.Compile();
        }

Összességében érthető. Bejárjuk a tulajdonságokat, delegáltakat hozunk létre, amelyek a settereket hívják, és eltároljuk. Ezután hívjuk őket, amikor szükség van rájuk.

A "Lassú" (Slow előtag a benchmarkokban):

        protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
        {
            var contact = new Contact();
            foreach (var property in _properties)
            {
                var correlation = correlations.FirstOrDefault(x => x.PropertyName == property.Name);
                if (correlation?.Value == null)
                    continue;

                property.SetValue(contact, correlation.Value);
            }
            return contact;
        }

Itt azonnal bejárjuk a tulajdonságokat, és közvetlenül meghívjuk a SetValue-t.

A példa kedvéért és demonstrációként elkészítettem egy egyszerű módszert, amely közvetlenül a párkapcsolatok korrelációs értékeit írja az entitás mezőibe. Előtag – Manual.

Most pedig vegyük a BenchmarkDotNet-t és vizsgáljuk meg a teljesítményt. És hirtelen... (spoiler — ez nem a helyes eredmény, a részletek lentebb)

Sikertelen cikk a reflexió felgyorsításáról.

Mit látunk itt? A Fast előtagot viselő módszerek szinte minden futásnál lassabbak, mint a Slow előtagot viselők. Ez érvényes a memóriafoglalásra és a működési sebességre egyaránt. Másrészről a gyönyörű és elegáns térképezési megvalósítás, amely mindenhol a lehető legjobban használja a LINQ-ra tervezett módszereket, jelentősen rontja a teljesítményt. A különbség nagyságrendi. A tendencia nem változik a futások számának változásával. A különbség csupán a mértékekben van. A LINQ-nal 4 – 200-szor lassabb, a hulladék pedig körülbelül hasonló arányban nő.

FRISSÍTVE

Nem hittem a szememnek, de ami még fontosabb, hogy sem a saját szememnek, sem a kódomnak nem hitte el kollégánk — Dmitry Tikhonov 0x1000000. Miután újraellenőrizte a megoldásomat, nagyszerűen felfedezte és rámutatott egy hibára, amelyet a végrehajtás során végrehajtott számos módosítás miatt a kezdeti és a végső implementáció között kihagytam. A Moq beállításban felfedezett hiba javítása után az összes eredmény a helyére került. A újbóli tesztelés eredményeinél a fő tendencia nem változik – a LINQ továbbra is nagyobb hatással van a teljesítményre, mint a reflexió. Azonban örömteli, hogy az Expression-ök fordításával végzett munka nem volt hiábavaló, és az eredmény látható a memóriafoglalásban és a végrehajtási időben is. Az első indítás, amikor a statikus mezők inicializálódnak, a „gyors” módszernél értelemszerűen lassabb, de később a helyzet megváltozik.

Itt van a újbóli tesztelés eredménye:

Sikertelen cikk a reflexió felgyorsításáról.

Következtetés: vállalati környezetben a reflexió használatakor különös trükkökre nincs szükség – a LINQ jelentősen csökkenti a teljesítményt. Ennek ellenére a magas terhelésű módszerek esetében, amelyek optimalizálást igényelnek, a reflexiót megőrizhetjük inicializálókként és delegátum fordítókként, amelyek később a „gyors” logikát biztosítják. Így megőrizheti a reflexió rugalmasságát és az alkalmazás gyorsaságát.

A benchmarkkód itt elérhető. Minden érdeklődő ellenőrizheti a szavaimat:
HabraReflectionTests

PS: A tesztekhez használt kód IoC-t alkalmaz, míg a benchmarkokhoz explicite építkezik. A végső megvalósításban levágtam minden olyan tényezőt, ami befolyásolhatja a teljesítményt és zavarossá teheti az eredményt.

PPS: Köszönet a felhasználónak Dmitry Tikhonov @0x1000000 a Moq konfigurációm hibájának felfedezéséért, ami hatással volt az első mérésekre. Ha valakinek a kedves olvasók közül elég karmája van, kérlek like-oljátok őt. Az illető megállt, belemerült, újraellenőrizte és felhívta a figyelmet a hibára. Úgy gondolom, hogy ez tiszteletre és szimpátiára méltó.

PPPS: Köszönet annak a precíz olvasónak, aki a stílushoz és megjelenéshez ragaszkodott. Az egységességért és kényelmességért vagyok felelős. A diplomáciai bemutatás hagy némi kívánnivalót maga után, de a kritikát figyelembe vettem. Kérem, közelítsenek a célszerszámhoz.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster