Neuspešen članek o pospeševanju refleksije

Takoj razložim naslov članka. Prvotni načrt je bil dati dober, zanesljiv nasvet o tem, kako pospešiti uporabo refleksije na preprostem, a realističnem primeru, vendar se je med primerjalno analizo izkazalo, da refleksija ni tako počasna, kot sem mislil, LINQ je počasnejši kot v mojih nočnih morah. A na koncu se je izkazalo, da sem se zmotila tudi pri merah ... Podrobnosti te življenjske zgodbe so pod rezom in v komentarjih. Ker je primer precej običajen in načeloma izveden tako, kot se običajno izvaja v podjetju, se je izkazalo za precej zanimivo, kot se mi zdi, prikaz življenja: vpliv na hitrost glavnega predmeta članka je bil ni opazen zaradi zunanje logike: Moq, Autofac, EF Core in drugi "bandings".

Delati sem začel pod vtisom tega članka: Zakaj je Reflection počasen

Kot lahko vidite, avtor predlaga uporabo prevedenih delegatov namesto neposrednega klicanja metod tipa refleksije kot odličen način za močno pospešitev aplikacije. Seveda obstaja emisija IL, vendar bi se ji rad izognil, saj je to najbolj delovno intenziven način za izvedbo naloge, ki je polna napak.

Glede na to, da sem vedno imel podobno mnenje o hitrosti refleksije, avtorjevih zaključkov nisem imel posebnega namena dvomiti.

Pogosto se srečujem z naivno uporabo refleksije v podjetju. Tip je zajet. Podatki o nepremičnini so vzeti. Pokliče se metoda SetValue in vsi se veselijo. Vrednost je prispela v ciljno polje, vsi zadovoljni. Zelo pametni ljudje - starejši in vodje ekip - pišejo svoje razširitve za objekt, ki temeljijo na tako naivni implementaciji "univerzalnih" preslikav ene vrste v drugo. Bistvo je običajno naslednje: vzamemo vsa polja, vzamemo vse lastnosti, jih preletimo: če se imena članov tipa ujemajo, izvedemo SetValue. Občasno ujamemo izjeme zaradi napak, kjer v enem od tipov nismo našli lastnosti, vendar tudi tukaj obstaja izhod, ki izboljša zmogljivost. Poskusi/ulovi.

Videl sem ljudi, kako na novo izumljajo razčlenjevalnike in preslikave, ne da bi bili popolnoma oboroženi z informacijami o tem, kako delujejo stroji pred njimi. Videl sem, kako ljudje svoje naivne izvedbe skrivajo za strategijami, za vmesniki, za injekcijami, kot da bi to opravičilo kasnejšo bakanalijo. Ob takih spoznanjih sem vihal nos. Pravzaprav nisem izmeril dejanskega uhajanja zmogljivosti in, če je bilo mogoče, sem preprosto spremenil izvedbo v bolj »optimalno«, če sem jo lahko dobil. Zato so me prve spodaj obravnavane meritve močno zmedle.

Mislim, da ste mnogi med branjem Richterja ali drugih ideologov naleteli na povsem pošteno trditev, da je refleksija v kodi pojav, ki izjemno negativno vpliva na delovanje aplikacije.

Klicanje refleksije prisili CLR, da gre skozi sklope, da najde tistega, ki ga potrebujejo, izvleče njihove metapodatke, jih razčleni itd. Poleg tega refleksija med prečkanjem zaporedij vodi do dodelitve velike količine pomnilnika. Porabimo pomnilnik, CLR odkrije GC in frize se začnejo. Moralo bi biti občutno počasno, verjemite mi. Ogromne količine pomnilnika na sodobnih produkcijskih strežnikih ali strojih v oblaku ne preprečijo velikih zamud pri obdelavi. Pravzaprav je več pomnilnika, večja je verjetnost, da boste OPAZILI, kako deluje GC. Odsev je v teoriji zanj dodatna rdeča cunja.

Vsi pa uporabljamo vsebnike IoC in preslikave datumov, katerih princip delovanja prav tako temelji na refleksiji, o njihovi zmogljivosti pa običajno ni vprašanj. Ne, ne zato, ker sta uvedba odvisnosti in abstrakcije od zunanjih omejenih kontekstnih modelov tako potrebni, da moramo v vsakem primeru žrtvovati zmogljivost. Vse je preprostejše - res ne vpliva veliko na učinkovitost.

Dejstvo je, da najpogostejša ogrodja, ki temeljijo na refleksijski tehnologiji, uporabljajo najrazličnejše trike za optimalnejše delo z njo. Ponavadi je to predpomnilnik. Običajno so to izrazi in delegati, prevedeni iz izraznega drevesa. Isti samodejni preslikavec vzdržuje konkurenčen slovar, ki ujema tipe s funkcijami, ki lahko pretvorijo eno v drugo, ne da bi priklicali refleksijo.

Kako se to doseže? V bistvu se to ne razlikuje od logike, ki jo sama platforma uporablja za ustvarjanje kode JIT. Ko se metoda prvič pokliče, se prevede (in ja, ta proces ni hiter); pri naslednjih klicih se nadzor prenese na že prevedeno metodo in ne bo bistvenega zmanjšanja zmogljivosti.

V našem primeru lahko uporabite tudi prevajanje JIT in nato uporabite prevedeno vedenje z enako zmogljivostjo kot njegovi primerki AOT. V tem primeru nam bodo na pomoč priskočili izrazi.

Zadevno načelo je mogoče na kratko formulirati na naslednji način:
Končni rezultat refleksije morate shraniti v predpomnilnik kot delegat, ki vsebuje prevedeno funkcijo. Prav tako je smiselno predpomniti vse potrebne objekte z informacijami o vrsti v poljih vašega tipa, delavca, ki so shranjena zunaj objektov.

V tem je logika. Zdrava pamet nam pravi, da če je nekaj mogoče prevesti in shraniti v predpomnilnik, potem je to treba storiti.

Če pogledamo naprej, je treba reči, da ima predpomnilnik pri delu z refleksijo svoje prednosti, tudi če ne uporabljate predlagane metode sestavljanja izrazov. Pravzaprav tukaj zgolj ponavljam teze avtorja članka, na katerega se sklicujem zgoraj.

Zdaj o kodi. Poglejmo si primer, ki temelji na moji nedavni bolečini, s katero sem se moral soočiti v resni produkciji resne kreditne institucije. Vse entitete so izmišljene, da ne bi kdo uganil.

Nekaj ​​bistva je. Naj bo Stik. Obstajajo črke s standardiziranim telesom, iz katerih razčlenjevalnik in hidrator ustvarita iste kontakte. Prispelo je pismo, prebrali smo ga, razčlenili na pare ključ-vrednost, ustvarili kontakt in ga shranili v bazo podatkov.

Osnovno je. Recimo, da ima stik lastnosti Polno ime, Starost in Kontaktni telefon. Ti podatki so posredovani v pismu. Podjetje želi tudi podporo, da bi lahko hitro dodala nove ključe za preslikavo lastnosti entitete v pare v telesu pisma. V primeru, da se je nekdo zmotil v predlogi ali če je pred izdajo potrebno nujno zagnati preslikavo pri novem partnerju in se prilagoditi novemu formatu. Nato lahko dodamo novo korelacijo preslikave kot poceni popravek podatkov. Se pravi življenjski primer.

Izvajamo, ustvarjamo teste. dela.

Kode ne bom posredoval: virov je veliko in na voljo so na GitHubu prek povezave na koncu članka. Lahko jih nalagaš, mučiš do nerazpoznavnosti in meriš, kot bi to vplivalo v tvojem primeru. Podal bom samo kodo dveh predlog metod, ki razlikujeta hidrator, ki naj bi bil hiter, od hidratorja, ki naj bi bil počasen.

Logika je naslednja: metoda predloge sprejme pare, ki jih ustvari osnovna logika razčlenjevalnika. Plast LINQ je razčlenjevalnik in osnovna logika hidratorja, ki naredi zahtevo v kontekstu baze podatkov in primerja ključe s pari iz razčlenjevalnika (za te funkcije obstaja koda brez LINQ za primerjavo). Nato se pari prenesejo na glavno metodo hidracije in vrednosti parov se nastavijo na ustrezne lastnosti entitete.

»Hitro« (predpona Hitro v merilih uspešnosti):

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

Kot lahko vidimo, je uporabljena statična zbirka z lastnostmi nastavitve - prevedene lambde, ki kličejo entiteto nastavitve. Ustvarjeno z naslednjo kodo:

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

Na splošno je jasno. Prečkamo lastnosti, ustvarimo delegate zanje, ki pokličejo nastavitve, in jih shranimo. Potem pokličemo, ko je treba.

»Počasi« (predpona Slow v merilih uspešnosti):

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

Tukaj takoj obidemo lastnosti in neposredno pokličemo SetValue.

Zaradi jasnosti in kot referenca sem implementiral naivno metodo, ki zapisuje vrednosti njihovih korelacijskih parov neposredno v polja entitet. Predpona – ročno.

Zdaj pa vzemimo BenchmarkDotNet in preučimo zmogljivost. In nenadoma ... (spoiler - to ni pravi rezultat, podrobnosti so spodaj)

Neuspešen članek o pospeševanju refleksije

Kaj vidimo tukaj? Metode, ki zmagoslavno nosijo predpono Fast, se izkažejo za počasnejše v skoraj vseh prehodih kot metode s predpono Slow. To velja tako za razporeditev kot za hitrost dela. Po drugi strani pa lepa in elegantna izvedba preslikave z uporabo temu namenjenih metod LINQ, kjer koli je to mogoče, nasprotno močno zmanjša produktivnost. Razlika je redna. Trend se ne spreminja z različnim številom prehodov. Edina razlika je v obsegu. Z LINQ je 4-200-krat počasnejši, smeti je več na približno enakem obsegu.

POSODOBLJENO

Nisem verjel svojim očem, a kar je še pomembneje, naš kolega ni verjel ne mojim očem ne moji kodi - Dmitrij Tihonov 0x1000000. Ko je dvakrat preveril mojo rešitev, je briljantno odkril in opozoril na napako, ki sem jo spregledal zaradi številnih sprememb v implementaciji, od začetnih do končnih. Po odpravi najdene napake v nastavitvi Moq so vsi rezultati padli na svoje mesto. Glede na rezultate ponovnega testiranja se glavni trend ne spremeni - LINQ še vedno bolj vpliva na zmogljivost kot refleksija. Je pa lepo, da delo s prevajanjem Expression ni opravljeno zaman in je rezultat viden tako v dodelitvi kot v času izvajanja. Prvi zagon, ko se inicializirajo statična polja, je pri »hitri« metodi seveda počasnejši, potem pa se situacija spremeni.

Tukaj je rezultat ponovnega preizkusa:

Neuspešen članek o pospeševanju refleksije

Zaključek: pri uporabi refleksije v podjetju ni posebne potrebe po trikih - LINQ bo bolj požrl produktivnost. Vendar pa lahko pri metodah z visoko obremenitvijo, ki zahtevajo optimizacijo, refleksijo shranite v obliki inicializatorjev in delegiranih prevajalnikov, ki bodo nato zagotovili "hitro" logiko. Tako lahko ohranite fleksibilnost refleksije in hitrost aplikacije.

Primerjalna koda je na voljo tukaj. Vsak lahko preveri moje besede:
HabraReflectionTests

PS: koda v testih uporablja IoC, v merilih uspešnosti pa eksplicitni konstrukt. Dejstvo je, da sem pri končni izvedbi izločil vse dejavnike, ki bi lahko vplivali na zmogljivost in naredili rezultat hrupen.

PPS: Hvala uporabniku Dmitrij Tihonov @0x1000000 za odkritje moje napake pri nastavitvi Moq, ki je vplivala na prve meritve. Če ima kdo od bralcev dovolj karme, naj ga všečka. Človek se je ustavil, moški prebral, moški dvakrat preveril in opozoril na napako. Mislim, da je to vredno spoštovanja in sočutja.

PPPS: hvala natančnemu bralcu, ki je slogu in dizajnu prišel do dna. Sem za enotnost in udobje. Diplomatičnost predstavitve pušča veliko želenega, vendar sem upošteval kritike. Prosim za projektil.

Vir: www.habr.com

Dodaj komentar