Mislykket artikkel om akselererende refleksjon

La meg forklare artikkelens tittel med en gang. I utgangspunktet planla jeg Ä gi gode, pÄlitelige rÄd om hvordan man fÄr fart pÄ refleksjon ved hjelp av et enkelt, men realistisk eksempel. Under benchmarking viste det seg imidlertid at refleksjonen ikke var sÄ treg som jeg trodde, og LINQ var tregere enn jeg noen gang hadde forestilt meg. Og det viste seg at jeg ogsÄ hadde gjort en mÄlefeil... Detaljene i denne virkelige historien finner du under snittet og i kommentarfeltet. Siden eksemplet er ganske hverdagslig og implementert pÄ en mÄte som vanligvis gjÞres i bedrifter, viste det seg Ä vÊre en ganske interessant demonstrasjon, etter min mening: effekten pÄ ytelsen til artikkelens hovedemne var ikke merkbar pÄ grunn av ekstern logikk: Moq, Autofac, EF Core og andre "bindinger".

Jeg begynte Ă„ jobbe under inntrykk av denne artikkelen: Hvorfor er refleksjon treg

Som du kan se, foreslÄr forfatteren Ä bruke kompilerte delegater i stedet for Ä fÄ direkte tilgang til refleksjonsmetoder som en utmerket mÄte Ä Þke applikasjonsytelsen betydelig. SelvfÞlgelig finnes det ogsÄ IL-utslipp, men det er best Ä unngÄ det, da det er den mest arbeidskrevende og feilutsatte mÄten Ä utfÞre oppgaven pÄ.

Siden jeg alltid har hatt en lignende oppfatning om refleksjonshastighet, hadde jeg ikke til hensikt Ä stille spÞrsmÄl ved forfatterens konklusjoner.

Jeg mĂžter ofte naiv bruk av refleksjon i bedriften. En type tas. Egenskapsinformasjon hentes. SetValue-metoden kalles, og alle er fornĂžyde. Verdien har landet i mĂ„lfeltet, alle er fornĂžyde. Smarte folk – seniorer og teamledere – skriver sine egne utvidelser til objektet, og baserer sine "universelle" tilordninger fra Ă©n type til en annen pĂ„ denne naive implementeringen. Hovedpoenget er vanligvis dette: ta alle feltene, ta alle egenskapene, iterer over dem: hvis typemedlemsnavnene samsvarer, kjĂžrer vi SetValue. Vi fanger med jevne mellomrom unntak nĂ„r en egenskap ikke finnes for en av typene, men selv her finnes det en mĂ„te Ă„ forbedre ytelsen pĂ„: PrĂžv/fang.

Jeg har sett folk gjenoppfinne parsere og mappere uten Ä vÊre fullt informert om hvordan hjulene som ble oppfunnet fÞr dem fungerte. Jeg har sett folk skjule sine naive implementeringer bak strategier, grensesnitt og injeksjoner, som om det ville unnskylde det pÄfÞlgende kaoset. Jeg rynket pÄ nesen av slike implementeringer. Faktisk mÄlte jeg ikke det faktiske ytelsesforbruket, og nÄr det var mulig, byttet jeg ganske enkelt ut implementeringen med en mer "optimal" en da jeg hadde tid. SÄ de fÞrste mÄlingene, som diskuteres nedenfor, forvirret meg alvorlig.

Jeg tror mange av dere, mens dere har lest Richter eller andre ideologer, har kommet over den helt berettigede pÄstanden om at refleksjon i kode er et fenomen som har en ekstremt negativ innvirkning pÄ ytelsen til en applikasjon.

Å kalle refleksjon tvinger CLR-en til Ă„ gĂ„ gjennom sammenstillinger for Ă„ finne den rette, hente metadataene deres, analysere den, og sĂ„ videre. Videre fĂžrer refleksjon under sekvensgjennomgang til store minneallokeringer. Vi forbruker minne, CLR pakker ut GC-en og fryser deretter. Dette burde vĂŠre merkbart tregt, tro meg. De enorme mengdene minne i moderne produksjonsservere eller skymaskiner forhindrer ikke hĂžye prosesseringsforsinkelser. Faktisk, jo mer minne du har, desto stĂžrre er sannsynligheten for at du vil LEGG MERKE TIL GC-ens ytelse. Refleksjon er i teorien et unĂždvendig rĂždt flagg for det.

Likevel bruker vi alle bĂ„de IoC-containere og datamappere, som ogsĂ„ er avhengige av refleksjon, men ytelsen deres pĂ„virkes vanligvis ikke. Det er ikke fordi avhengighetsinjeksjon og abstraksjon fra eksterne avgrensede kontekstmodeller er sĂ„ nĂždvendige at vi uansett mĂ„ ofre ytelse. Det er enklere – de pĂ„virker egentlig ikke ytelsen nevneverdig.

Faktum er at de vanligste rammeverkene basert pÄ refleksjon bruker alle slags triks for Ä optimalisere bruken. Vanligvis involverer dette en hurtigbuffer. Uttrykk og delegater kompilert fra uttrykkstrÊr er ogsÄ vanlige. Automapperen vedlikeholder for eksempel en samtidig ordbok som mapper typer til funksjoner som kan konverteres til hverandre uten Ä ty til refleksjon.

Hvordan oppnÄs dette? I hovedsak er det ikke annerledes enn logikken plattformen selv bruker for Ä generere JIT-kode. FÞrste gang en metode kalles, kompileres den (og ja, denne prosessen er ikke rask). EtterfÞlgende kall overfÞrer kontrollen til den kompilerte metoden, og det vil ikke bli noen betydelig ytelsestap.

I vÄrt tilfelle kan vi ogsÄ dra nytte av JIT-kompilering og deretter bruke den kompilerte oppfÞrselen med samme ytelse som dens AOT-motparter. Uttrykk vil komme til unnsetning i dette tilfellet.

Prinsippet det gjelder kan kort formuleres slik:
Det endelige resultatet av refleksjonen bÞr mellomlagres som en delegat som inneholder den kompilerte funksjonen. Det er ogsÄ fornuftig Ä mellomlagre alle nÞdvendige objekter med typeinformasjon i felt av arbeidstypen din som er lagret eksternt.

Det er logikk i dette. Sunn fornuft sier oss at hvis noe kan kompileres og mellomlagres, sÄ bÞr det gjÞres det.

NÄr man ser fremover, bÞr det bemerkes at mellomlagring har sine fordeler nÄr man jobber med refleksjon, selv uten Ä bruke den foreslÄtte uttrykkskompileringsmetoden. Faktisk gjentar jeg her bare poengene til forfatteren av artikkelen jeg lenket til ovenfor.

NÄ, over til koden. La oss se pÄ et eksempel basert pÄ et nylig smertepunkt jeg mÞtte i et stÞrre produksjonsmiljÞ hos en stor finansinstitusjon. Alle enheter er fiktive, sÄ ingen vil gjette.

Det finnes en bestemt enhet. La oss kalle den Kontakt. Det finnes e-poster med standardiserte brĂždtekster, som parseren og hydratoren oppretter disse kontaktene fra. En e-post kommer inn, vi leser den, analyserer den i nĂžkkelverdipar, oppretter en kontakt og lagrer den i databasen.

Det er enkelt. La oss si at en kontakt har egenskaper som fullt navn, alder og telefonnummer. Disse dataene overfÞres i e-posten. Bedriften Þnsker ogsÄ stÞtte for raskt Ä kunne legge til nye nÞkler for Ä tilordne enhetsegenskaper til par i e-postens brÞdtekst. Dette er i tilfelle noen gjÞr en skrivefeil i malen, eller hvis tilordningen mÄ lanseres raskt fra en ny partner fÞr utgivelsen, og tilpasses et nytt format. Da kan vi legge til en ny tilordningskorrelasjon som en billig datafiks. SÄ dette er et eksempel fra virkeligheten.

Vi implementerer det, lager tester. Det fungerer.

Jeg vil ikke vise koden: det finnes mye kildekode, og den er tilgjengelig pÄ GitHub via lenken pÄ slutten av artikkelen. Du kan laste den ned, finjustere den til det ugjenkjennelige og mÄle hvordan det vil pÄvirke tilfellet ditt. Jeg vil bare vise koden for to malmetoder som skiller hydratoren som skulle vÊre rask fra hydratoren som skulle vÊre treg.

Logikken er som fĂžlger: malmetoden mottar par generert av parseren sin kjernelogikk. LINQ-laget er parseren og hydratorens kjernelogikk, som spĂžr databasekonteksten og matcher nĂžkler med parseren sin par (det finnes ikke-LINQ-kode for disse funksjonene for sammenligning). Parene sendes deretter til hovedhydreringsmetoden, og parverdiene settes til de tilsvarende entitetsegenskapene.

"Rask" (prefikset Rask i referansetester):

 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, brukes en statisk samling med egenskapssettere – kompilerte lambdaer som kaller enhetens setter. De opprettes med 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();
        }

Det er generelt sett klart. Vi gÄr gjennom egenskapene i lÞkker, oppretter delegater for dem, kaller settere og lagrer dem. Deretter kaller vi dem nÄr det er nÞdvendig.

"Slow" (Slow-prefikset i referansetester):

        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 omgÄr vi umiddelbart egenskapene og kaller SetValue direkte.

For klarhetens skyld og som et referansepunkt implementerte jeg en naiv metode som skriver verdiene til korrelasjonsparene deres direkte til entitetsfelt. Prefikset er Manuell.

La oss nÄ ta BenchmarkDotNet og teste ytelsen. Og plutselig ... (spoiler alert: dette er et feil resultat; detaljer nedenfor)

Mislykket artikkel om akselererende refleksjon

Hva ser vi her? Metoder som triumferende bĂŠrer prefikset Fast er tregere enn metoder med prefikset Slow i nesten alle passeringer. Dette gjelder bĂ„de allokerings- og utfĂžrelseshastighet. PĂ„ den annen side reduserer en vakker og elegant mapping-implementering, som bruker LINQ-metoder nĂ„r det er mulig, ytelsen betydelig. Forskjellen er stĂžrrelsesordener. Denne trenden endres ikke med ulikt antall passeringer. Den eneste forskjellen er skala. Med LINQ er det 4–200 ganger tregere, med omtrent samme mengde sĂžppel.

OPPDATERT

Jeg kunne ikke tro mine egne Ăžyne, men enda viktigere, kollegaen vĂ„r trodde verken mine egne Ăžyne eller koden min - Dmitrij Tikhonov 0x1000000Etter Ă„ ha testet lĂžsningen min pĂ„ nytt, oppdaget og pĂ„pekte han pĂ„ en strĂ„lende mĂ„te en feil jeg hadde oversett pĂ„ grunn av en rekke endringer i den fĂžrste implementeringen. Etter Ă„ ha fikset den oppdagede feilen i Moq-oppsettet, gikk alle resultatene tilbake til det normale. Resultatene fra den nye testingen viser at hovedtrenden forblir uendret – LINQ pĂ„virker fortsatt ytelsen mer enn refleksjon. Det er imidlertid fint Ă„ se at arbeidet med Ă„ kompilere uttrykk er verdt det, og resultatene er synlige bĂ„de i allokering og utfĂžrelsestid. Den fĂžrste kjĂžringen, nĂ„r statiske felt initialiseres, er naturlig nok tregere for den "raske" metoden, men deretter endrer situasjonen seg.

Her er resultatet av retesten:

Mislykket artikkel om akselererende refleksjon

Konklusjon: NĂ„r man bruker refleksjon i bedriften, er det ikke nĂždvendig Ă„ ty til triks – LINQ vil redusere ytelsen betydelig. I metoder med hĂžy belastning som krever optimalisering, kan imidlertid refleksjon bevares i form av initialiseringsverktĂžy og delegeringskompilatorer, som deretter gir "rask" logikk. PĂ„ denne mĂ„ten kan du bevare bĂ„de fleksibiliteten til refleksjonen og hastigheten til applikasjonen din.

Referansekoden er tilgjengelig her. Alle som er interesserte kan dobbeltsjekke uttalelsene mine:
HabraReflectionTester

PS: Koden i testene bruker IoC, mens benchmark-testene bruker en eksplisitt konstruksjon. Dette er fordi jeg i den endelige implementeringen eliminerte alle faktorer som kunne pÄvirke ytelsen og forvrenge resultatene.

PPS: Takk til brukeren Dmitrij Tikhonov @0x1000000 For Ä ha oppdaget feilen min i Moq-oppsettet, som pÄvirket de fÞrste mÄlingene. Hvis noen lesere har nok karma, vennligst gi den en like. Noen stoppet, noen leste nÞye, noen dobbeltsjekket og pÄpekte feilen. Jeg synes dette fortjener respekt og sympati.

PPPS: Takk til den omhyggelige leseren som gravde seg inn i stilen og layouten. Jeg er helt for konsistens og brukervennlighet. Presentasjonens diplomati er mye Ă„ Ăžnske, men jeg har tatt kritikken til etterretning. Vennligst sett i gang.

Kilde: www.habr.com

KjĂžp pĂ„litelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere đŸ”„ KjĂžp pĂ„litelig webhotell med DDoS-beskyttelse, VPS VDS-servere | ProHoster