Ebaõnnestunud artikkel peegelduse kiirendamise kohta

Selgitan kohe artikli pealkirja. Algne plaan oli anda lihtsal, kuid realistlikul näitel head usaldusväärset nõu refleksiooni kasutamise kiirendamiseks, kuid võrdlusuuringu käigus selgus, et peegeldus ei olegi nii aeglane kui arvasin, LINQ on aeglasem kui mu õudusunenägudes. Aga lõpuks selgus, et tegin ka mõõtmistes vea... Selle eluloo detailid on lõike all ja kommentaarides. Kuna näide on üsna tavaline ja põhimõtteliselt rakendatav, nagu tavaliselt ettevõttes tehakse, osutus see minu arvates üsna huvitavaks eludemonstratsiooniks: mõju artikli põhiteema kiirusele oli välise loogika tõttu pole märgata: Moq, Autofac, EF Core ja teised "rihmad".

Alustasin tööd selle artikli mulje all: Miks on peegeldus aeglane

Nagu näete, soovitab autor peegeldustüüpi meetodite otsese kutsumise asemel kasutada kompileeritud delegaate, mis on suurepärane viis rakenduse märkimisväärselt kiirendamiseks. Muidugi on IL-emissioon, kuid ma tahaksin seda vältida, kuna see on kõige töömahukam viis ülesande täitmiseks, mis on täis vigu.

Arvestades, et olen refleksiooni kiiruse osas alati sarnasel seisukohal, ei kavatsenud ma autori järeldusi eriti kahtluse alla seada.

Tihti kohtan ettevõttes naiivset refleksiooni kasutamist. Tüüp on võetud. Võetakse infot kinnistu kohta. Kutsutakse välja SetValue meetod ja kõik rõõmustavad. Väärtus on sihtväljale saabunud, kõik on rahul. Väga targad inimesed - pensionärid ja meeskonnajuhid - kirjutavad oma laiendusi objektile, tuginedes sellisele naiivsele teostusele, ühte tüüpi "universaalsed" kaardistajad. Põhiolemus on tavaliselt järgmine: võtame kõik väljad, võtame kõik omadused, itereerime neid üle: kui tüübiliikmete nimed ühtivad, käivitame SetValue. Aeg-ajalt tabame eksimuste tõttu erandeid, kus me mõnest tüübist mõnda omadust ei leidnud, kuid ka siin leidub jõudlust parandav väljapääs. Proovi/võta kinni.

Olen näinud, kuidas inimesed leiutavad parsereid ja kaardistajaid uuesti, ilma et nad oleksid täielikult varustatud teabega, kuidas enne neid masinad töötavad. Olen näinud, kuidas inimesed peidavad oma naiivsed teostused strateegiate, liideste ja süstide taha, justkui vabandaks see järgnevat bakhhanaaliat. Pöörasin selliste tõdemuste peale nina püsti. Tegelikult ma tegelikku jõudluse leket ei mõõtnud ja võimalusel muutsin teostuse lihtsalt "optimaalsema" vastu, kui sain kätte. Seetõttu ajasid esimesed allpool käsitletavad mõõtmised mind tõsiselt segadusse.

Ma arvan, et paljud teist, lugedes Richterit või teisi ideolooge, on kohanud täiesti õiglast väidet, et koodis kajastamine on nähtus, millel on rakenduse toimivusele äärmiselt negatiivne mõju.

Peegelduse kutsumine sunnib CLR-i läbima koostu, et leida vajalik, tõmmata üles nende metaandmed, sõeluda neid jne. Lisaks põhjustab jadade läbimisel peegeldumine suure hulga mälu eraldamist. Me kasutame mälu tühjaks, CLR avab GC ja friisid algavad. Uskuge mind, see peaks olema märgatavalt aeglane. Kaasaegsete tootmisserverite või pilveseadmete tohutud mälumahud ei hoia ära suuri töötlemise viivitusi. Tegelikult, mida rohkem mälu, seda tõenäolisemalt MÄRKATE, kuidas GC töötab. Peegeldus on tema jaoks teoreetiliselt eriti punane kalts.

Küll aga kasutame me kõik IoC konteinereid ja kuupäevade kaardistajaid, mille tööpõhimõte põhineb samuti peegeldusel, kuid nende toimimises tavaliselt küsimusi ei teki. Ei, mitte sellepärast, et sõltuvuste juurutamine ja välistest piiratud kontekstimudelitest abstraktsioon on nii vajalikud, et me peame jõudluse igal juhul ohverdama. Kõik on lihtsam - see ei mõjuta jõudlust palju.

Fakt on see, et kõige levinumad peegeldustehnoloogial põhinevad raamistikud kasutavad kõikvõimalikke nippe, et sellega optimaalsemalt töötada. Tavaliselt on see vahemälu. Tavaliselt on need avaldised ja avaldisepuust koostatud delegaadid. Sama automaatne kaardistaja haldab konkurentsivõimelist sõnastikku, mis sobitab tüübid funktsioonidega, mis võivad ühe teisendada ilma peegeldust kutsumata.

Kuidas see saavutatakse? Põhimõtteliselt ei erine see loogikast, mida platvorm ise JIT-koodi genereerimiseks kasutab. Kui meetodit kutsutakse esimest korda, siis see kompileeritakse (ja jah, see protsess ei ole kiire); järgmistel väljakutsetel viiakse juhtimine üle juba koostatud meetodile ja olulisi jõudluse langusi ei toimu.

Meie puhul saate kasutada ka JIT-i kompileerimist ja seejärel kasutada kompileeritud käitumist sama jõudlusega kui selle AOT kolleegid. Väljendid tulevad meile sel juhul appi.

Kõnealuse põhimõtte võib lühidalt sõnastada järgmiselt:
Peaksite peegelduse lõpptulemuse vahemällu salvestama koostatud funktsiooni sisaldava delegaadina. Samuti on mõttekas hoida vahemällu kõik vajalikud objektid koos tüübiteabega teie tüübi, töötaja väljadele, mis on salvestatud väljaspool objekte.

Selles on loogikat. Terve mõistus ütleb meile, et kui midagi saab kompileerida ja vahemällu salvestada, siis tuleb seda teha.

Tulevikku vaadates tuleks öelda, et peegeldusega töötamisel on vahemällul oma eelised, isegi kui te ei kasuta pakutud avaldiste koostamise meetodit. Tegelikult kordan siin lihtsalt artikli autori teese, millele eespool viitan.

Nüüd koodist. Vaatame näidet, mis põhineb minu hiljutisel valul, millega pidin silmitsi seisma tõsise krediidiasutuse tõsises lavastuses. Kõik olemid on fiktiivsed, nii et keegi ei oska arvata.

Mingi olemus on olemas. Olgu kontakt. Seal on standardse kehaga tähed, millest parser ja hüdraat loovad need samad kontaktid. Saabus kiri, lugesime selle läbi, sõelusime võtme-väärtuse paarideks, lõime kontakti ja salvestasime selle andmebaasi.

See on elementaarne. Oletame, et kontaktil on atribuudid täisnimi, vanus ja kontakttelefon. Need andmed edastatakse kirjas. Ettevõte soovib ka tuge, et saaks kiiresti lisada uusi võtmeid olemi atribuutide paarideks vastendamiseks kirja põhiosas. Juhul, kui keegi tegi mallis kirjavea või kui enne väljalaskmist on vaja kiiresti käivitada kaardistamine uuelt partnerilt, kohandades seda uue vorminguga. Seejärel saame odava andmeparandusena lisada uue kaardistamiskorrelatsiooni. Ehk siis näide elust.

Rakendame, loome teste. Töötab.

Ma ei anna koodi: allikaid on palju ja need on GitHubis saadaval artikli lõpus oleva lingi kaudu. Saate neid laadida, tundmatuseni piinata ja mõõta, nagu see teie puhul mõjutaks. Annan ainult kahe mallimeetodi koodi, mis eristavad hüdraati, mis pidi olema kiire, hüdraatorist, mis pidi olema aeglane.

Loogika on järgmine: mallimeetod võtab vastu põhilise parseriloogika poolt genereeritud paarid. LINQ kiht on parser ja hüdraatori põhiloogika, mis teeb päringu andmebaasi konteksti ja võrdleb võtmeid parseri paaridega (nende funktsioonide jaoks on võrdluseks kood ilma LINQta). Järgmisena suunatakse paarid põhihüdratatsioonimeetodile ja paaride väärtused seatakse olemi vastavatele omadustele.

„Kiire” (etaldusnäitajate eesliide Fast):

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

Nagu näeme, kasutatakse setteri omadustega staatilist kogumit – kompileeritud lambdasid, mis kutsuvad setteri olemit. Loodud järgmise koodiga:

        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();
        }

Üldiselt on asi selge. Me läbime atribuudid, loome neile delegaadid, kes kutsuvad seadjaid, ja salvestame need. Siis helistame vajadusel.

„Aeglane” (etaldusaluste eesliide Aeglane):

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

Siin läheme kohe atribuutidest mööda ja helistame otse SetValue-le.

Selguse huvides ja viitena rakendasin naiivse meetodi, mis kirjutab nende korrelatsioonipaaride väärtused otse olemiväljadele. Eesliide – manuaal.

Võtame nüüd BenchmarkDotNeti ja uurime toimivust. Ja äkki... (spoiler - see pole õige tulemus, üksikasjad allpool)

Ebaõnnestunud artikkel peegelduse kiirendamise kohta

Mida me siin näeme? Meetodid, mis triumfeerivalt kannavad kiiret eesliidet, osutuvad peaaegu kõigis käikudes aeglasemaks kui aeglase eesliitega meetodid. See kehtib nii jaotamise kui ka töö kiiruse kohta. Seevastu ilus ja elegantne LINQ-meetoditega kaardistamise teostus, kus vähegi võimalik, selleks mõeldud, vastupidi, vähendab oluliselt tootlikkust. Erinevus on järjekorras. Trend ei muutu erineva läbistuste arvuga. Ainus erinevus on mastaabis. LINQ-ga on see 4-200 korda aeglasem, prügi on ligikaudu samas mahus rohkem.

UUENDATUD

Ma ei uskunud oma silmi, aga mis veelgi olulisem, meie kolleeg ei uskunud ei minu silmi ega koodi - Dmitri Tihhonov 0x1000000. Olles minu lahendust üle kontrollinud, avastas ta suurepäraselt ja juhtis tähelepanu veale, mis mul jäi mitmete muudatuste tõttu teostuses, algusest kuni lõplikuni, kahe silma vahele. Pärast Moqi seadistuses leitud vea parandamist langesid kõik tulemused paika. Kordustesti tulemuste järgi peamine trend ei muutu – LINQ mõjutab jõudlust ikka rohkem kui peegeldust. Küll aga on tore, et Expressioni kompileerimisega ei tehta asjata ning tulemust on näha nii jaotus- kui ka täitmisajas. Esimene käivitamine, kui staatilised väljad initsialiseeritakse, on "kiire" meetodi puhul loomulikult aeglasem, kuid seejärel olukord muutub.

Siin on kordustesti tulemus:

Ebaõnnestunud artikkel peegelduse kiirendamise kohta

Järeldus: ettevõttes peegelduse kasutamisel pole erilist vajadust trikkide appi võtta - LINQ sööb tootlikkust rohkem. Suure koormusega meetodite puhul, mis nõuavad optimeerimist, saate aga salvestada peegeldust lähtestajate ja delegeeritud kompilaatorite näol, mis tagavad seejärel "kiire" loogika. Nii saate säilitada nii peegelduse paindlikkuse kui ka rakenduse kiiruse.

Võrdluskoodi kood on saadaval siin. Igaüks võib mu sõnu üle kontrollida:
Habrapeegelduse testid

PS: testides olev kood kasutab IoC-d ja võrdlusnäitajates selgesõnalist konstruktsiooni. Fakt on see, et lõplikus teostuses lõikasin ära kõik tegurid, mis võivad jõudlust mõjutada ja tulemuse lärmakaks muuta.

PPS: Tänud kasutajale Dmitri Tihhonov @0x1000000 selle eest, et avastasin oma vea Moqi seadistamisel, mis mõjutas esimesi mõõtmisi. Kui kellelgi lugejatest on piisavalt karmat, siis palun meeldige. Mees peatus, mees luges, mees kontrollis üle ja juhtis tähelepanu veale. Ma arvan, et see on austust ja kaastunnet väärt.

PPPS: tänu hoolikale lugejale, kes sai stiili ja kujunduse põhja. Olen ühtsuse ja mugavuse poolt. Ettekande diplomaatia jätab soovida, kuid kriitikaga arvestasin. Ma küsin mürsku.

Allikas: www.habr.com

Lisa kommentaar