Neuspješan članak o ubrzanju refleksije

Odmah ću objasniti naslov članka. Izvorni plan je bio dati dobar, pouzdan savjet kako ubrzati korištenje refleksije koristeći jednostavan, ali realan primjer, ali tijekom benchmarkinga pokazalo se da refleksija nije tako spora kao što sam mislio, LINQ je sporiji nego u mojim noćnim morama. No, na kraju se pokazalo da sam i ja pogriješila u mjerama... Detalji ove životne priče nalaze se ispod rubrike i u komentarima. Budući da je primjer sasvim uobičajen i načelno implementiran kao što se obično radi u poduzeću, pokazao se vrlo zanimljiv, kako mi se čini, prikaz života: utjecaj na brzinu glavnog predmeta članka bio je nije vidljivo zbog vanjske logike: Moq, Autofac, EF Core i drugi "bandings".

Počeo sam raditi pod dojmom ovog članka: Zašto je Reflection spor

Kao što možete vidjeti, autor predlaže korištenje kompiliranih delegata umjesto izravnog pozivanja metoda tipa refleksije kao odličan način da se znatno ubrza aplikacija. Postoji, naravno, emisija IL-a, ali bih je želio izbjeći, jer je to najzahtjevniji način za obavljanje zadatka, koji je prepun grešaka.

S obzirom da sam oduvijek bio sličnog mišljenja o brzini promišljanja, nisam imao posebnu namjeru propitivati ​​autorove zaključke.

Često se susrećem s naivnom uporabom refleksije u poduzeću. Tip je uzet. Uzimaju se podaci o nekretnini. Poziva se metoda SetValue i svi se raduju. Vrijednost je stigla u ciljno polje, svi su zadovoljni. Vrlo pametni ljudi - stariji i voditelji timova - pišu svoja proširenja za objekt, temeljeći se na takvoj naivnoj implementaciji "univerzalnih" preslikača jednog tipa na drugi. Suština je obično sljedeća: uzmemo sva polja, uzmemo sva svojstva, iteriramo preko njih: ako se imena članova tipa podudaraju, izvršavamo SetValue. S vremena na vrijeme hvatamo iznimke zbog pogrešaka gdje nismo pronašli neko svojstvo u jednom od tipova, ali čak i ovdje postoji izlaz koji poboljšava performanse. Pokušaj uhvatiti.

Vidio sam ljude kako ponovno izmišljaju parsere i mapere, a da nisu bili potpuno naoružani informacijama o tome kako rade strojevi koji su došli prije njih. Vidio sam kako ljudi skrivaju svoje naivne implementacije iza strategija, iza sučelja, iza injekcija, kao da bi to opravdalo kasniju bakanaliju. Vrteo sam nos na takve spoznaje. Zapravo, nisam mjerio stvarno curenje performansi i, ako je bilo moguće, jednostavno sam promijenio implementaciju na "optimalniju" ako sam je se mogao dočepati. Stoga su me prva mjerenja o kojima se raspravlja u nastavku ozbiljno zbunila.

Mislim da su mnogi od vas, čitajući Richtera ili druge ideologe, naišli na potpuno poštenu izjavu da je refleksija u kodu fenomen koji ima izrazito negativan utjecaj na performanse aplikacije.

Pozivanje refleksije prisiljava CLR da prođe kroz sklopove kako bi pronašao onaj koji im treba, izvukao njihove metapodatke, analizirao ih itd. Nadalje, refleksija tijekom obilaska nizova dovodi do dodjele velike količine memorije. Trošimo memoriju, CLR otkriva GC i frizovi počinju. Trebalo bi biti osjetno sporo, vjerujte mi. Ogromne količine memorije na modernim proizvodnim poslužiteljima ili strojevima u oblaku ne sprječavaju velika kašnjenja obrade. Zapravo, što je više memorije, veća je vjerojatnost da ćete PRIMIJETITI kako GC radi. Odraz mu je, u teoriji, dodatna crvena krpa.

Međutim, svi mi koristimo IoC spremnike i mapere datuma, čiji se princip rada također temelji na refleksiji, ali obično nema pitanja o njihovoj izvedbi. Ne, ne zato što su uvođenje ovisnosti i apstrakcija iz vanjskih ograničenih kontekstnih modela toliko potrebni da u svakom slučaju moramo žrtvovati izvedbu. Sve je jednostavnije - stvarno ne utječe puno na performanse.

Činjenica je da najčešći okviri koji se temelje na tehnologiji refleksije koriste razne trikove kako bi s njom optimalnije radili. Obično je ovo predmemorija. Obično su to izrazi i delegati sastavljeni iz stabla izraza. Isti automapper održava konkurentni rječnik koji povezuje tipove s funkcijama koje mogu pretvoriti jedan u drugi bez pozivanja refleksije.

Kako se to postiže? U biti, to se ne razlikuje od logike koju sama platforma koristi za generiranje JIT koda. Kada se metoda pozove prvi put, ona se kompajlira (i, da, ovaj proces nije brz); pri sljedećim pozivima kontrola se prenosi na već kompiliranu metodu i neće biti značajnih padova performansi.

U našem slučaju, također možete koristiti JIT kompilaciju i zatim koristiti kompilirano ponašanje s istom izvedbom kao i njegovi AOT parnjaci. Izrazi će nam u ovom slučaju priskočiti u pomoć.

Načelo o kojem je riječ može se ukratko formulirati na sljedeći način:
Trebali biste predmemorirati konačni rezultat refleksije kao delegat koji sadrži kompajliranu funkciju. Također ima smisla predmemorirati sve potrebne objekte s informacijama o tipu u poljima vašeg tipa, radnika, koja su pohranjena izvan objekata.

Ima logike u tome. Zdrav razum nam govori da ako se nešto može kompajlirati i pohraniti u predmemoriju, onda to treba učiniti.

Gledajući unaprijed, treba reći da predmemorija u radu s refleksijom ima svoje prednosti, čak i ako ne koristite predloženu metodu sastavljanja izraza. Zapravo, ovdje jednostavno ponavljam teze autora članka na koji se gore pozivam.

Sada o kodu. Pogledajmo primjer koji se temelji na mojoj nedavnoj boli s kojom sam se morao suočiti u ozbiljnoj produkciji ozbiljne kreditne institucije. Svi entiteti su fiktivni da nitko ne bi pogodio.

Postoji neka suština. Neka bude Kontakt. Postoje slova sa standardiziranim tijelom, od kojih parser i hidrator stvaraju te iste kontakte. Stiglo je pismo, pročitali smo ga, raščlanili na parove ključ-vrijednost, stvorili kontakt i spremili ga u bazu podataka.

Elementarno je. Recimo da kontakt ima svojstva Puno ime, Dob i Kontakt telefon. Ovi podaci se prenose u pismu. Tvrtka također želi podršku za brzo dodavanje novih ključeva za mapiranje svojstava entiteta u parove u tijelu pisma. U slučaju da je netko pogriješio u predlošku ili ako je prije objave potrebno hitno pokrenuti mapiranje od novog partnera, prilagođavajući se novom formatu. Tada možemo dodati novu korelaciju preslikavanja kao jeftini ispravak podataka. Odnosno životni primjer.

Implementiramo, stvaramo testove. Djela.

Neću dati kod: postoji mnogo izvora, a dostupni su na GitHubu preko veze na kraju članka. Možete ih puniti, mučiti do neprepoznatljivosti i mjeriti, kako bi to djelovalo u vašem slučaju. Dat ću samo šifru dvije predloške metode koje razlikuju hidrator koji je trebao biti brz od hidratatora koji je trebao biti spor.

Logika je sljedeća: metoda predloška prima parove generirane osnovnom logikom parsera. LINQ sloj je parser i osnovna logika hidratora, koji postavlja zahtjev kontekstu baze podataka i uspoređuje ključeve s parovima iz parsera (za ove funkcije postoji kod bez LINQ-a za usporedbu). Zatim se parovi prosljeđuju glavnoj metodi hidratacije, a vrijednosti parova postavljaju se na odgovarajuća svojstva entiteta.

“Brzo” (prefiks Brzo u mjerilima):

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

Kao što vidimo, koristi se statična kolekcija sa svojstvima postavljača - kompilirane lambde koje pozivaju entitet postavljača. Kreirano pomoću sljedećeg koda:

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

Općenito je jasno. Prolazimo kroz svojstva, stvaramo delegate za njih koji pozivaju postavljače i spremamo ih. Onda zovemo kada je potrebno.

“Sporo” (prefiks Sporo u mjerilima):

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

Ovdje odmah zaobilazimo svojstva i izravno pozivamo SetValue.

Radi jasnoće i kao referenca, implementirao sam naivnu metodu koja upisuje vrijednosti njihovih korelacijskih parova izravno u polja entiteta. Prefiks – Ručno.

Uzmimo sada BenchmarkDotNet i ispitajmo performanse. I odjednom... (spoiler - ovo nije točan rezultat, detalji su ispod)

Neuspješan članak o ubrzanju refleksije

Što vidimo ovdje? Metode koje trijumfalno nose prefiks Fast ispadaju sporije u gotovo svim prolazima od metoda s prefiksom Slow. To vrijedi i za raspodjelu i za brzinu rada. S druge strane, lijepa i elegantna implementacija mapiranja pomoću LINQ metoda namijenjenih tome gdje god je to moguće, naprotiv, uvelike smanjuje produktivnost. Razlika je reda. Trend se ne mijenja s različitim brojem prolaza. Jedina razlika je u mjerilu. S LINQ-om je 4 - 200 puta sporiji, ima više smeća na približno istom razmjeru.

NADOGRADILI

Nisam vjerovao svojim očima, ali što je još važnije, naš kolega nije vjerovao ni mojim očima ni mojoj šifri - Dmitrij Tihonov 0x1000000. Nakon što je još jednom provjerio moje rješenje, sjajno je otkrio i ukazao na grešku koju sam propustio zbog brojnih promjena u implementaciji, od početnih do konačnih. Nakon ispravljanja pronađenog buga u postavci Moqa, svi rezultati su došli na svoje mjesto. Prema rezultatima ponovnog testiranja, glavni trend se ne mijenja - LINQ i dalje utječe na izvedbu više nego refleksija. No, lijepo je što posao sa kompajliranjem izraza nije uzaludan, a rezultat je vidljiv iu dodjeli i vremenu izvršenja. Prvo pokretanje, kada se inicijaliziraju statička polja, prirodno je sporije za "brzu" metodu, ali onda se situacija mijenja.

Ovo je rezultat ponovnog testiranja:

Neuspješan članak o ubrzanju refleksije

Zaključak: kada koristite refleksiju u poduzeću, nema posebne potrebe za pribjegavanjem trikovima - LINQ će više pojesti produktivnost. Međutim, u visokoopterećenim metodama koje zahtijevaju optimizaciju, refleksiju možete spremiti u obliku inicijalizatora i delegatskih kompajlera, koji će tada pružiti "brzu" logiku. Na taj način možete održati i fleksibilnost refleksije i brzinu aplikacije.

Benchmark kod dostupan je ovdje. Svatko može provjeriti moje riječi:
HabraReflectionTests

PS: kod u testovima koristi IoC, a u mjerilima koristi eksplicitnu konstrukciju. Činjenica je da sam u konačnoj implementaciji isključio sve čimbenike koji bi mogli utjecati na izvedbu i učiniti rezultat bučnim.

PPS: Hvala korisniku Dmitrij Tihonov @0x1000000 za otkrivanje moje greške u postavljanju Moq-a, koja je utjecala na prva mjerenja. Ako netko od čitatelja ima dovoljno karme, neka ga lajka. Čovjek stao, čovjek pročitao, čovjek dvaput provjerio i ukazao na grešku. Mislim da je ovo vrijedno poštovanja i simpatije.

PPPS: hvala pedantnom čitatelju koji je došao do dna stila i dizajna. Ja sam za uniformnost i praktičnost. Diplomatičnost prezentacije ostavlja mnogo za poželjeti, ali sam uzeo u obzir kritike. Tražim projektil.

Izvor: www.habr.com

Dodajte komentar