Mislykket artikkel om akselererende refleksjon

Jeg skal umiddelbart forklare tittelen på artikkelen. Den opprinnelige planen var å gi gode, pålitelige råd om hvordan man kan få fart på bruken av refleksjon ved hjelp av et enkelt, men realistisk eksempel, men under benchmarking viste det seg at refleksjon ikke er så sakte som jeg trodde, LINQ er tregere enn i mine mareritt. Men til slutt viste det seg at jeg også gjorde en feil i målingene... Detaljer om denne livshistorien er under kuttet og i kommentarfeltet. Siden eksemplet er ganske vanlig og implementert i prinsippet slik det vanligvis gjøres i en bedrift, viste det seg å være en ganske interessant, slik det virker for meg, demonstrasjon av livet: innvirkningen på hastigheten til hovedemnet i artikkelen var ikke merkbar på grunn av ekstern logikk: Moq, Autofac, EF Core og andre "bandinger".

Jeg begynte å jobbe under inntrykket av denne artikkelen: Hvorfor er refleksjon treg

Som du kan se, foreslår forfatteren å bruke kompilerte delegater i stedet for direkte å kalle metoder for refleksjonstype som en flott måte å øke hastigheten på applikasjonen. Det er selvfølgelig IL-utslipp, men det vil jeg gjerne unngå, siden dette er den mest arbeidskrevende måten å utføre oppgaven på, som er full av feil.

Med tanke på at jeg alltid har hatt en lignende oppfatning om refleksjonshastigheten, hadde jeg ikke særlig til hensikt å stille spørsmål ved forfatterens konklusjoner.

Jeg møter ofte naiv bruk av refleksjon i virksomheten. Typen er tatt. Informasjon om eiendommen er tatt. SetValue-metoden kalles og alle gleder seg. Verdien har kommet i målfeltet, alle er fornøyde. Veldig smarte mennesker - seniorer og teamledere - skriver utvidelsene sine til objekt, basert på en slik naiv implementering "universelle" kartleggere av en type til en annen. Essensen er vanligvis dette: vi tar alle feltene, tar alle egenskapene, itererer over dem: hvis navnene på typemedlemmene samsvarer, kjører vi SetValue. Fra tid til annen fanger vi opp unntak på grunn av feil der vi ikke fant noe eiendom i en av typene, men også her er det en utvei som forbedrer ytelsen. Prøv/fang.

Jeg har sett folk gjenoppfinne parsere og kartleggere uten å være fullt bevæpnet med informasjon om hvordan maskinene som kom før dem fungerer. Jeg har sett folk skjule sine naive implementeringer bak strategier, bak grensesnitt, bak injeksjoner, som om dette ville unnskylde den påfølgende bakkanalien. Jeg vendte nesen opp av slike erkjennelser. Faktisk målte jeg ikke den virkelige ytelseslekkasjen, og om mulig endret jeg ganske enkelt implementeringen til en mer "optimal" hvis jeg kunne få tak i den. Derfor forvirret de første målingene som ble diskutert nedenfor meg alvorlig.

Jeg tror mange av dere, som leser Richter eller andre ideologer, har kommet over et helt rettferdig utsagn om at refleksjon i kode er et fenomen som har en ekstremt negativ innvirkning på ytelsen til applikasjonen.

Å kalle refleksjon tvinger CLR til å gå gjennom sammenstillinger for å finne den de trenger, hente opp metadataene deres, analysere dem osv. I tillegg fører refleksjon mens du krysser sekvenser til tildeling av en stor mengde minne. Vi bruker opp minnet, CLR avdekker GC og friser begynner. Det skal være merkbart tregt, tro meg. De enorme mengdene minne på moderne produksjonsservere eller skymaskiner forhindrer ikke store behandlingsforsinkelser. Faktisk, jo mer minne, jo mer sannsynlig er det at du legger merke til hvordan GC fungerer. Refleksjon er i teorien en ekstra rød fille for ham.

Imidlertid bruker vi alle IoC-beholdere og datokartleggere, hvis driftsprinsipp også er basert på refleksjon, men det er vanligvis ingen spørsmål om ytelsen. Nei, ikke fordi innføring av avhengigheter og abstraksjon fra eksterne begrensede kontekstmodeller er så nødvendig at vi uansett må ofre ytelse. Alt er enklere - det påvirker egentlig ikke ytelsen mye.

Faktum er at de vanligste rammeverkene som er basert på refleksjonsteknologi bruker alle mulige triks for å jobbe mer optimalt med det. Vanligvis er dette en cache. Vanligvis er dette uttrykk og delegater kompilert fra uttrykkstreet. Den samme automapperen opprettholder en konkurrerende ordbok som matcher typer med funksjoner som kan konvertere hverandre til hverandre uten å kalle refleksjon.

Hvordan oppnås dette? I hovedsak er dette ikke forskjellig fra logikken som plattformen selv bruker for å generere JIT-kode. Når en metode kalles for første gang, blir den kompilert (og ja, denne prosessen er ikke rask); på etterfølgende samtaler overføres kontrollen til den allerede kompilerte metoden, og det vil ikke være noen vesentlige ytelsesavkortninger.

I vårt tilfelle kan du også bruke JIT-kompilering og deretter bruke den kompilerte virkemåten med samme ytelse som AOT-motpartene. Ytringer vil komme oss til hjelp i denne saken.

Prinsippet det gjelder kan kort formuleres slik:
Du bør cache det endelige resultatet av refleksjon som en delegat som inneholder den kompilerte funksjonen. Det er også fornuftig å bufre alle nødvendige objekter med typeinformasjon i feltene til typen din, arbeideren, som er lagret utenfor objektene.

Det er logikk i dette. Sunn fornuft forteller oss at hvis noe kan kompileres og bufres, så bør det gjøres.

Ser vi fremover, skal det sies at cachen i arbeid med refleksjon har sine fordeler, selv om du ikke bruker den foreslåtte metoden for å kompilere uttrykk. Her gjentar jeg faktisk tesene til forfatteren av artikkelen som jeg refererer til ovenfor.

Nå om koden. La oss se på et eksempel som er basert på mine nylige smerter som jeg måtte møte i en seriøs produksjon av en seriøs kredittinstitusjon. Alle enheter er fiktive slik at ingen kan gjette.

Det er noe essens. La det være kontakt. Det er bokstaver med en standardisert kropp, som parseren og hydratoren lager de samme kontaktene fra. Et brev kom, vi leste det, analyserte det i nøkkelverdi-par, opprettet en kontakt og lagret det i databasen.

Det er elementært. La oss si at en kontakt har egenskapene Fullt navn, Alder og Kontakttelefon. Disse dataene overføres i brevet. Virksomheten ønsker også støtte for raskt å kunne legge til nye nøkler for kartlegging av enhetsegenskaper i par i hoveddelen av brevet. I tilfelle noen har skrevet en skrivefeil i malen, eller hvis det før utgivelsen er nødvendig å raskt starte kartlegging fra en ny partner, tilpasse seg det nye formatet. Deretter kan vi legge til en ny kartleggingskorrelasjon som en billig datafiks. Altså et livseksempel.

Vi implementerer, lager tester. Virker.

Jeg vil ikke gi koden: det er mange kilder, og de er tilgjengelige på GitHub via lenken på slutten av artikkelen. Du kan laste dem, torturere dem til det ugjenkjennelige og måle dem, slik det vil påvirke i ditt tilfelle. Jeg vil bare gi koden til 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 den grunnleggende parserlogikken. LINQ-laget er parseren og den grunnleggende logikken til hydratoren, som sender en forespørsel til databasekonteksten og sammenligner nøkler med par fra parseren (for disse funksjonene er det kode uten LINQ for sammenligning). Deretter sendes parene til hovedhydreringsmetoden, og verdiene til parene settes til de tilsvarende egenskapene til enheten.

"Rask" (Prefiks Rask 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, brukes en statisk samling med setter-egenskaper - kompilerte lambdaer som kaller setter-entiteten. Laget av 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 krysser eiendommene, oppretter delegater for dem som ringer settere, og lagrer dem. Så ringer vi ved behov.

"Slow" (prefiks Slow 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 omgår vi eiendommene umiddelbart og ringer SetValue direkte.

For klarhet og som referanse implementerte jeg en naiv metode som skriver verdiene til deres korrelasjonspar direkte inn i enhetsfeltene. Prefiks – Manuell.

La oss nå ta BenchmarkDotNet og undersøke ytelsen. Og plutselig... (spoiler - dette er ikke riktig resultat, detaljer er nedenfor)

Mislykket artikkel om akselererende refleksjon

Hva ser vi her? Metoder som triumferende bærer Fast-prefikset viser seg å være tregere i nesten alle omganger enn metoder med Slow-prefikset. Dette gjelder både for tildeling og arbeidshastighet. På den annen side, en vakker og elegant implementering av kartlegging ved hjelp av LINQ-metoder beregnet for dette der det er mulig, tvert imot, reduserer produktiviteten betraktelig. Forskjellen er i orden. Trenden endres ikke med ulikt antall pasninger. Den eneste forskjellen er i skala. Med LINQ er det 4 - 200 ganger tregere, det er mer søppel i omtrent samme skala.

OPPDATERT

Jeg trodde ikke mine øyne, men enda viktigere, vår kollega trodde verken mine øyne eller koden min - Dmitry Tikhonov 0x1000000. Etter å ha dobbeltsjekket løsningen min, oppdaget og påpekte han en feil som jeg gikk glipp av på grunn av en rekke endringer i implementeringen, fra første til siste. Etter å ha fikset den funnet feilen i Moq-oppsettet, falt alle resultatene på plass. I følge retestresultatene endres ikke hovedtrenden – LINQ påvirker fortsatt ytelsen mer enn refleksjon. Det er imidlertid hyggelig at arbeidet med Expression-kompilering ikke gjøres forgjeves, og resultatet er synlig både i tildeling og gjennomføringstid. Den første lanseringen, når statiske felt initialiseres, er naturlig nok tregere for den "raske" metoden, men så endrer situasjonen seg.

Her er resultatet av retesten:

Mislykket artikkel om akselererende refleksjon

Konklusjon: når du bruker refleksjon i en bedrift, er det ikke noe særlig behov for å ty til triks - LINQ vil spise opp produktiviteten mer. Men i høybelastningsmetoder som krever optimalisering, kan du lagre refleksjon i form av initialisatorer og delegerte kompilatorer, som da vil gi "rask" logikk. På denne måten kan du opprettholde både fleksibiliteten til refleksjon og hastigheten på applikasjonen.

Referansekoden er tilgjengelig her. Hvem som helst kan dobbeltsjekke ordene mine:
HabraReflectionTests

PS: koden i testene bruker IoC, og i benchmarkene bruker den en eksplisitt konstruksjon. Faktum er at i den endelige implementeringen kuttet jeg av alle faktorer som kan påvirke ytelsen og gjøre resultatet støyende.

PPS: Takk til brukeren Dmitry Tikhonov @0x1000000 for å oppdage feilen min med å sette opp Moq, som påvirket de første målingene. Hvis noen av leserne har tilstrekkelig karma, vennligst lik det. Mannen stoppet, mannen leste, mannen dobbeltsjekket og påpekte feilen. Jeg synes dette er verdig respekt og sympati.

PPPS: takk til den grundige leseren som kom til bunns i stilen og designet. Jeg er for enhetlighet og bekvemmelighet. Diplomatiet i presentasjonen etterlater mye å være ønsket, men jeg tok kritikken til etterretning. Jeg ber om prosjektilet.

Kilde: www.habr.com

Legg til en kommentar