Mislykket artikel om accelererende refleksion

Jeg vil straks forklare artiklens titel. Den oprindelige plan var at give gode, pålidelige råd om, hvordan man kan fremskynde brugen af ​​refleksion ved hjælp af et simpelt, men realistisk eksempel, men under benchmarking viste det sig, at refleksion ikke er så langsom, som jeg troede, LINQ er langsommere end i mine mareridt. Men til sidst viste det sig, at jeg også lavede en fejl i målingerne... Detaljer om denne livshistorie er under klippet og i kommentarerne. Da eksemplet er ret almindeligt og implementeret i princippet, som man normalt gør i en virksomhed, viste det sig at være en ganske interessant, som det forekommer mig, demonstration af livet: Indvirkningen på hastigheden af ​​artiklens hovedemne var ikke mærkbar på grund af ekstern logik: Moq, Autofac, EF Core og andre "stropper".

Jeg begyndte at arbejde under indtryk af denne artikel: Hvorfor er Reflektion langsom

Som du kan se, foreslår forfatteren at bruge kompilerede delegerede i stedet for direkte at kalde refleksionstypemetoder som en fantastisk måde at fremskynde applikationen meget. Der er selvfølgelig IL-emission, men det vil jeg gerne undgå, da det er den mest arbejdskrævende måde at udføre opgaven på, som er behæftet med fejl.

I betragtning af, at jeg altid har haft en lignende mening om refleksionshastigheden, havde jeg ikke specielt til hensigt at stille spørgsmålstegn ved forfatterens konklusioner.

Jeg møder ofte naiv brug af refleksion i virksomheden. Typen er taget. Oplysninger om ejendommen tages. SetValue-metoden kaldes og alle glæder sig. Værdien er ankommet i målfeltet, alle er glade. Meget smarte mennesker - seniorer og teamledere - skriver deres udvidelser til objekt, baseret på en sådan naiv implementering "universelle" kortlæggere af en type til en anden. Essensen er normalt denne: vi tager alle felterne, tager alle egenskaberne, itererer over dem: hvis navnene på typemedlemmerne matcher, udfører vi SetValue. Fra tid til anden fanger vi undtagelser på grund af fejl, hvor vi ikke fandt noget ejendom i en af ​​typerne, men selv her er der en vej ud, der forbedrer ydeevnen. Prøv/fang.

Jeg har set folk genopfinde parsere og kortlæggere uden at være fuldt bevæbnet med information om, hvordan de maskiner, der kom før dem, fungerer. Jeg har set folk skjule deres naive implementeringer bag strategier, bag grænseflader, bag injektioner, som om dette ville undskylde den efterfølgende bacchanalia. Jeg vendte næsen op over sådanne erkendelser. Faktisk målte jeg ikke den reelle ydeevnelækage, og hvis det var muligt, ændrede jeg simpelthen implementeringen til en mere "optimal", hvis jeg kunne få fingrene i den. Derfor forvirrede de første målinger, der diskuteres nedenfor, mig alvorligt.

Jeg tror, ​​at mange af jer, der læser Richter eller andre ideologer, er stødt på et helt fair udsagn om, at refleksion i kode er et fænomen, der har en ekstrem negativ indflydelse på applikationens ydeevne.

Kaldende refleksion tvinger CLR til at gå gennem samlinger for at finde den, de har brug for, trække deres metadata frem, analysere dem osv. Derudover fører refleksion, mens du krydser sekvenser, til allokering af en stor mængde hukommelse. Vi bruger hukommelsen, CLR afslører GC'en og friserne begynder. Det burde være mærkbart langsomt, tro mig. De enorme mængder hukommelse på moderne produktionsservere eller cloud-maskiner forhindrer ikke store behandlingsforsinkelser. Faktisk, jo mere hukommelse, jo mere sandsynligt er det, at du MÆRKER, hvordan GC'en fungerer. Refleksion er i teorien en ekstra rød klud for ham.

Vi bruger dog alle IoC-containere og datokortlæggere, hvis funktionsprincip også er baseret på refleksion, men der er normalt ingen spørgsmål om deres ydeevne. Nej, ikke fordi indførelsen af ​​afhængigheder og abstraktion fra eksterne begrænsede kontekstmodeller er så nødvendige, at vi under alle omstændigheder er nødt til at ofre ydeevne. Alt er enklere - det påvirker virkelig ikke ydeevnen meget.

Faktum er, at de mest almindelige rammer, der er baseret på refleksionsteknologi, bruger alle mulige tricks til at arbejde mere optimalt med det. Normalt er dette en cache. Typisk er disse udtryk og delegerede kompileret fra udtrykstræet. Den samme automapper opretholder en konkurrencedygtig ordbog, der matcher typer med funktioner, der kan konvertere den ene til en anden uden at kalde refleksion.

Hvordan opnås dette? I det væsentlige er dette ikke anderledes end den logik, som platformen selv bruger til at generere JIT-kode. Når en metode kaldes for første gang, kompileres den (og ja, denne proces er ikke hurtig); ved efterfølgende opkald overføres kontrollen til den allerede kompilerede metode, og der vil ikke være nogen væsentlige ydelsesnedsættelser.

I vores tilfælde kan du også bruge JIT-kompilering og derefter bruge den kompilerede adfærd med samme ydeevne som dens AOT-modstykker. Udtryk vil komme os til hjælp i dette tilfælde.

Det pågældende princip kan kort formuleres således:
Du bør cache det endelige resultat af refleksion som en delegeret, der indeholder den kompilerede funktion. Det giver også mening at cache alle nødvendige objekter med typeinformation i felterne for din type, arbejderen, der er gemt uden for objekterne.

Det er der logik i. Sund fornuft fortæller os, at hvis noget kan kompileres og cachelagres, så skal det gøres.

Ser man fremad, skal det siges, at cachen i arbejdet med refleksion har sine fordele, selvom man ikke bruger den foreslåede metode til at kompilere udtryk. Faktisk gentager jeg her blot teserne fra forfatteren af ​​artiklen, som jeg henviser til ovenfor.

Nu om koden. Lad os se på et eksempel, der er baseret på min seneste smerte, som jeg måtte stå over for i en seriøs produktion af et seriøst kreditinstitut. Alle enheder er fiktive, så ingen ville gætte.

Der er noget essens. Lad der være kontakt. Der er bogstaver med en standardiseret krop, hvorfra parseren og hydratoren skaber de samme kontakter. Et brev ankom, vi læste det, analyserede det i nøgle-værdi-par, oprettede en kontakt og gemte det i databasen.

Det er elementært. Lad os sige, at en kontakt har egenskaberne Fuldt navn, Alder og Kontakttelefon. Disse data overføres i brevet. Virksomheden ønsker også støtte til hurtigt at kunne tilføje nye nøgler til at kortlægge enhedsegenskaber i par i brødteksten. I tilfælde af at nogen har lavet en tastefejl i skabelonen, eller hvis det inden udgivelsen er nødvendigt hurtigt at starte kortlægning fra en ny partner, tilpasse sig det nye format. Så kan vi tilføje en ny kortlægningskorrelation som et billigt datafix. Altså et livseksempel.

Vi implementerer, laver tests. Arbejder.

Jeg vil ikke give koden: der er mange kilder, og de er tilgængelige på GitHub via linket i slutningen af ​​artiklen. Du kan indlæse dem, torturere dem til ukendelighed og måle dem, som det ville påvirke i dit tilfælde. Jeg vil kun give koden for to skabelonmetoder, der adskiller hydratoren, som skulle være hurtig, fra hydratoren, som skulle være langsom.

Logikken er som følger: skabelonmetoden modtager par genereret af den grundlæggende parserlogik. LINQ-laget er parseren og den grundlæggende logik i hydratoren, som laver en anmodning til databasekonteksten og sammenligner nøgler med par fra parseren (for disse funktioner er der kode uden LINQ til sammenligning). Derefter overføres parrene til hovedhydreringsmetoden, og værdierne af parrene indstilles til enhedens tilsvarende egenskaber.

"Hurtig" (præfikset Hurtig i benchmarks):

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

Som vi kan se, bruges en statisk samling med setter-egenskaber - kompilerede lambdaer, der kalder setter-entiteten. Oprettet af følgende kode:

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

Generelt er det klart. Vi krydser ejendommene, opretter delegerede til dem, der kalder opstillere, og gemmer dem. Så ringer vi, når det er nødvendigt.

"Langsom" (præfikset Langsom i benchmarks):

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

Her går vi straks uden om ejendommene og ringer direkte til SetValue.

For klarhedens skyld og som reference implementerede jeg en naiv metode, der skriver værdierne af deres korrelationspar direkte ind i entitetsfelterne. Præfiks – Manual.

Lad os nu tage BenchmarkDotNet og undersøge ydeevnen. Og pludselig... (spoiler - dette er ikke det korrekte resultat, detaljer er nedenfor)

Mislykket artikel om accelererende refleksion

Hvad ser vi her? Metoder, der triumferende bærer Fast-præfikset, viser sig at være langsommere i næsten alle omgange end metoder med Langsom-præfikset. Dette gælder både for tildeling og arbejdshastighed. På den anden side reducerer en smuk og elegant implementering af kortlægning ved hjælp af LINQ-metoder beregnet til dette hvor det er muligt, tværtimod kraftigt produktiviteten. Forskellen er af orden. Tendensen ændrer sig ikke med forskellige antal afleveringer. Den eneste forskel er i skalaen. Med LINQ er det 4 - 200 gange langsommere, der er mere affald i nogenlunde samme skala.

OPDATERET

Jeg troede ikke mine øjne, men endnu vigtigere, vores kollega troede hverken mine øjne eller min kode - Dmitry Tikhonov 0x1000000. Efter at have dobbelttjekket min løsning, opdagede og påpegede han på glimrende vis en fejl, som jeg gik glip af på grund af en række ændringer i implementeringen, fra første til sidste. Efter at have rettet den fundne fejl i Moq-opsætningen faldt alle resultaterne på plads. Ifølge gentestresultaterne ændres hovedtendensen ikke - LINQ påvirker stadig ydeevnen mere end refleksion. Det er dog rart, at arbejdet med Expression compilation ikke er gjort forgæves, og resultatet er synligt både i allokering og eksekveringstid. Den første lancering, når statiske felter initialiseres, er naturligvis langsommere for den "hurtige" metode, men så ændrer situationen sig.

Her er resultatet af gentesten:

Mislykket artikel om accelererende refleksion

Konklusion: Når man bruger refleksion i en virksomhed, er der ikke noget særligt behov for at ty til tricks - LINQ vil æde produktiviteten mere op. Men i højbelastningsmetoder, der kræver optimering, kan du spare refleksion i form af initialiseringer og delegerede compilere, som så vil give "hurtig" logik. På denne måde kan du bevare både fleksibiliteten i refleksionen og påføringens hastighed.

Benchmark-koden er tilgængelig her. Enhver kan dobbelttjekke mine ord:
HabraReflectionTests

PS: koden i testene bruger IoC, og i benchmarks bruger den en eksplicit konstruktion. Faktum er, at jeg i den endelige implementering afskar alle faktorer, der kunne påvirke ydeevnen og gøre resultatet støjende.

PPS: Tak til brugeren Dmitry Tikhonov @0x1000000 for at opdage min fejl ved opsætning af Moq, som påvirkede de første målinger. Hvis nogen af ​​læserne har tilstrækkelig karma, så like det. Manden stoppede, manden læste, manden dobbelttjekkede og påpegede fejlen. Jeg synes, det er værdigt til respekt og sympati.

PPPS: tak til den omhyggelige læser, der kom til bunds i stilen og designet. Jeg går ind for ensartethed og bekvemmelighed. Præsentationens diplomati lader meget tilbage at ønske, men jeg tog kritikken i betragtning. Jeg beder om projektilet.

Kilde: www.habr.com

Tilføj en kommentar