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

Azonnal kifejtem a cikk címét. Az eredeti terv az volt, hogy egy egyszerű, de reális példával jó, megbízható tanácsokat adunk a reflexió használatának felgyorsítására, de a benchmarking során kiderült, hogy a reflexió nem olyan lassú, mint gondoltam, a LINQ lassabb, mint a rémálmaimban. De végül kiderült, hogy én is hibáztam a méréseknél... Ennek az élettörténetnek a részletei a kivágás alatt és a kommentekben. Mivel a példa meglehetősen hétköznapi, és elvileg úgy valósítható meg, mint általában egy vállalatnál, elég érdekes, számomra úgy tűnik, életbemutatónak bizonyult: a cikk fő témájának sebességére gyakorolt ​​hatása az volt, külső logika miatt nem észrevehető: Moq, Autofac, EF Core és egyéb "sávok".

Ennek a cikknek a hatására kezdtem el dolgozni: Miért lassú a tükrözés?

Amint látható, a szerző azt javasolja, hogy a reflexiós típusú metódusok közvetlen hívása helyett fordítsa le a küldötteket, mivel ez nagyszerű módja az alkalmazás gyorsításának. Természetesen van IL emisszió, de szeretném elkerülni, hiszen ez a legmunkaigényesebb módja a hibákkal teli feladat végrehajtásának.

Tekintettel arra, hogy a reflexió sebességével kapcsolatban mindig is hasonló véleményem volt, nem különösebben szándékoztam megkérdőjelezni a szerző következtetéseit.

Gyakran találkozom a reflexió naiv használatával a vállalkozásban. A típus felvett. Az ingatlanról tájékoztatást vesznek fel. Meghívják a SetValue metódust, és mindenki örül. Megérkezett az érték a célmezőbe, mindenki boldog. Nagyon okos emberek – idősek és csapatvezetők – az objektumhoz írják kiterjesztéseiket, ilyen naiv megvalósításra alapozva az egyik típusú „univerzális” leképezőket a másikra. A lényeg általában ez: kivesszük az összes mezőt, átvesszük az összes tulajdonságot, iterálunk rajtuk: ha a típustagok neve egyezik, akkor végrehajtjuk a SetValue-t. Időről időre elkapunk olyan hibákból adódó kivételeket, amikor valamelyik típusban nem találtunk valamilyen tulajdonságot, de itt is van kiút, ami javítja a teljesítményt. Próbáld ki/kapd el.

Láttam, hogy emberek újra feltalálták az elemzőket és leképezőket anélkül, hogy teljesen fel voltak fegyverezve az előttük lévő gépek működésével kapcsolatos információkkal. Láttam, hogy az emberek naiv megvalósításaikat stratégiák mögé, interfészek mögé, injekciók mögé rejtik, mintha ez felmentést jelentene a későbbi bakchanáliákra. Felkaptam az orrom az ilyen felismerésekre. Valójában nem mértem fel a valós teljesítményszivárgást, és ha lehetett, egyszerűen "optimálisabbra" változtattam a megvalósítást, ha a kezembe került. Ezért az alábbiakban tárgyalt első mérések komolyan megzavartak.

Azt hiszem, sokan közületek, Richtert vagy más ideológusokat olvasva találkoztak azzal a teljesen jogos kijelentéssel, hogy a kódban való tükröződés olyan jelenség, amely rendkívül negatív hatással van az alkalmazás teljesítményére.

A reflektálás hívása arra kényszeríti a CLR-t, hogy végigmenjen az összeállításokon, hogy megtalálja a szükségeset, előkeresse a metaadatait, elemezze őket stb. Ezenkívül a szekvenciák bejárása közbeni reflexió nagy mennyiségű memória lefoglalásához vezet. Felhasználjuk a memóriát, a CLR feltárja a GC-t és elkezdődnek a frízek. Érezhetően lassúnak kell lennie, hidd el. A modern éles szervereken vagy felhőalapú gépeken található hatalmas memóriamennyiség nem akadályozza meg a nagy feldolgozási késéseket. Valójában minél több memória, annál valószínűbb, hogy ÉSZREVÉSZI a GC működését. A reflexió elméletileg egy extra vörös rongy számára.

Mindazonáltal mindannyian használunk IoC konténereket és dátumleképezőket, amelyek működési elve szintén a reflexión alapul, de teljesítményükkel kapcsolatban általában nincs kérdés. Nem, nem azért, mert a függőségek bevezetése és a külső korlátozott kontextusmodellektől való elvonatkoztatás annyira szükséges, hogy a teljesítményt mindenképpen fel kell áldoznunk. Minden egyszerűbb – valójában nincs nagy hatással a teljesítményre.

A helyzet az, hogy a legelterjedtebb, reflexiós technológián alapuló keretrendszerek mindenféle trükköt bevetnek, hogy optimálisabban dolgozzanak vele. Általában ez egy gyorsítótár. Ezek általában a kifejezésfából összeállított kifejezések és delegáltak. Ugyanez az automatatérképező versenyképes szótárt tart fenn, amely a típusokat olyan függvényekkel párosítja, amelyek a reflexió hívása nélkül konvertálhatják egymást.

Hogyan érhető el ez? Ez lényegében nem különbözik attól a logikától, amelyet maga a platform használ a JIT kód generálására. A metódus első meghívásakor lefordításra kerül (és igen, ez a folyamat nem gyors), a következő hívásoknál az irányítás átkerül a már lefordított metódusra, és nem lesz jelentős teljesítménycsökkenés.

Esetünkben használhatja a JIT fordítást is, majd a lefordított viselkedést ugyanolyan teljesítménnyel használhatja, mint az AOT megfelelői. A kifejezések ebben az esetben a segítségünkre lesznek.

A kérdéses elv röviden a következőképpen fogalmazható meg:
A tükrözés végeredményét a lefordított függvényt tartalmazó delegáltként kell gyorsítótáraznia. Szintén célszerű az összes szükséges objektumot a típusinformációkkal együtt gyorsítótárazni a típusának, a feldolgozónak a mezőiben, amelyek az objektumon kívül vannak tárolva.

Ebben van logika. A józan ész azt súgja, hogy ha valamit le lehet fordítani és gyorsítótárban tárolni, akkor azt meg kell tenni.

A jövőre nézve azt kell mondani, hogy a tükrözéssel végzett gyorsítótárnak megvannak az előnyei, még akkor is, ha nem használja a kifejezések összeállításának javasolt módszerét. Valójában itt egyszerűen megismétlem a fent hivatkozott cikk szerzőjének téziseit.

Most a kódról. Nézzünk egy példát, amely a közelmúltbeli fájdalmamon alapul, amellyel egy komoly hitelintézet komoly produkciójában kellett szembenéznem. Minden entitás fiktív, így senki sem sejtheti.

Van valami esszencia. Legyen Kapcsolat. Vannak szabványos törzsű betűk, amelyekből az elemző és a hidratáló ugyanazokat az érintkezőket hozza létre. Egy levél érkezett, elolvastuk, kulcs-érték párokba elemeztük, létrehoztunk egy névjegyet, és elmentettük az adatbázisba.

Ez elemi. Tegyük fel, hogy egy kapcsolattartó a következő tulajdonságokkal rendelkezik: Teljes név, életkor és kapcsolattartó telefonszáma. Ezeket az adatokat a levélben továbbítják. A vállalkozás támogatást szeretne ahhoz is, hogy gyorsan hozzá tudjon adni új kulcsokat az entitástulajdonságok párokba rendezéséhez a levél törzsében. Abban az esetben, ha valaki elgépelést vétett a sablonban, vagy a megjelenés előtt sürgősen el kell indítani a leképezést egy új partnertől, alkalmazkodva az új formátumhoz. Ezután olcsó adatjavításként hozzáadhatunk egy új leképezési korrelációt. Vagyis egy életpélda.

Megvalósítunk, teszteket készítünk. Művek.

A kódot nem adom meg: sok forrás létezik, és a cikk végén található linken keresztül elérhetők a GitHubon. Feltöltheti, a felismerhetetlenségig megkínozhatja és mérheti őket, ahogy ez az Ön esetében is érintené. Csak két sablonmódszer kódját adom meg, amelyek megkülönböztetik a gyorsnak hitt hidratálót a lassúnak vélt hidratálótól.

A logika a következő: a template metódus az alapvető elemző logika által generált párokat fogadja. A LINQ réteg a hidratáló elemzője és alapvető logikája, amely kérést küld az adatbázis-környezethez, és összehasonlítja a kulcsokat az elemző párjaival (ezekhez a függvényekhez van LINQ nélküli kód az összehasonlításhoz). Ezután a párok átkerülnek a fő hidratációs módszerhez, és a párok értékeit az entitás megfelelő tulajdonságaira állítják be.

„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;
        }

Amint látjuk, egy statikus gyűjteményt használunk setter tulajdonságokkal - összeállított lambdákat, amelyek meghívják a setter entitást. A következő kóddal készült:

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

        private static Action<Contact, string> 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<Contact, string>>)Expression.Lambda(setterExp, paramEntity, paramValueOriginal);

            return lambda.Compile();
        }

Általában világos. Bejárjuk a tulajdonságokat, létrehozzuk számukra a beállítókat hívó delegátusokat, és elmentjük őket. Aztán hívunk, ha kell.

„Lassú” (Lassú 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 megkerüljük a tulajdonságokat, és közvetlenül hívjuk a SetValue-t.

Az áttekinthetőség kedvéért és referenciaként egy naiv módszert implementáltam, amely a korrelációs párjaik értékeit közvetlenül az entitásmezőkbe írja. Előtag – Kézi.

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

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

Mit látunk itt? Azok a módszerek, amelyek diadalmasan viselik a gyors előtagot, szinte minden lépésben lassabbnak bizonyulnak, mint a lassú előtagú módszerek. Ez igaz az elosztásra és a munka sebességére is. Ezzel szemben a LINQ módszerekkel, ahol csak lehetséges, a térképezés szép és elegáns megvalósítása, ahol csak lehetséges, éppen ellenkezőleg, nagymértékben csökkenti a termelékenységet. A különbség sorrendben van. A tendencia nem változik a különböző passzszámokkal. Az egyetlen különbség a léptékben van. A LINQ-val 4-200-szor lassabb, több a szemét körülbelül ugyanabban a léptékben.

FRISSÍTVE

Nem hittem a szememnek, de ami még fontosabb, kollégánk sem a szememnek, sem a kódomnak nem hitt - Dmitrij Tikhonov 0x1000000. Miután kétszer is ellenőrizte a megoldásomat, remekül felfedezett és rámutatott egy olyan hibára, amelyet a megvalósítás számos változtatása miatt elmulasztottam, az elsőtől a végsőig. A Moq beállításában talált hiba kijavítása után minden eredmény a helyére került. Az újrateszt eredményei szerint a fő trend nem változik – a LINQ továbbra is jobban befolyásolja a teljesítményt, mint a tükröződést. Jó azonban, hogy a kifejezések fordításával végzett munka nem megy hiába, és az eredmény mind az allokációban, mind a végrehajtási időben látható. Az első indítás, amikor a statikus mezőket inicializálják, természetesen lassabb a „gyors” módszernél, de aztán megváltozik a helyzet.

Íme az újrateszt eredménye:

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

Következtetés: ha egy vállalkozásban reflexiót használunk, nincs különösebb szükség trükkökre – a LINQ jobban felemészti a termelékenységet. Az optimalizálást igénylő nagy terhelésű módszerekben azonban megtakaríthatja a tükrözést inicializálók és delegált fordítók formájában, amelyek aztán „gyors” logikát biztosítanak. Így megőrizheti a tükrözés rugalmasságát és az alkalmazás sebességét.

A benchmark kód itt érhető el. Bárki ellenőrizheti a szavaimat:
HabraReflectionTests

PS: a tesztekben a kód IoC-t, a benchmarkokban pedig explicit konstrukciót használ. A helyzet az, hogy a végső megvalósításban levágtam minden olyan tényezőt, amely befolyásolhatja a teljesítményt, és zajossá teheti az eredményt.

PPS: Köszönet a felhasználónak Dmitrij Tikhonov @0x1000000 mert felfedeztem a Moq beállítási hibámat, ami az első méréseket érintette. Ha valamelyik olvasónak elegendő karmája van, kérem lájkolja. A férfi megállt, a férfi olvasott, a férfi kétszer is ellenőrizte, és rámutatott a hibára. Szerintem ez tiszteletet és együttérzést érdemel.

PPPS: köszönet az aprólékos olvasónak, aki a stílus és a dizájn mélyére ért. Én az egységességért és a kényelemért vagyok. Az előadás diplomáciája sok kívánnivalót hagy maga után, de a kritikát figyelembe vettem. kérem a lövedéket.

Forrás: will.com

Hozzászólás