Misslyckad artikel om att pÄskynda reflektion

Jag ska genast förklara artikelnamnet. Det var ursprungligen tÀnkt att ge ett bra och pÄlitligt rÄd för att pÄskynda anvÀndningen av refleksion genom ett enkelt, men realistiskt exempel. Men under benchmarking visade det sig att refleksion inte Àr sÄ lÄngsam som jag trodde, medan LINQ Àr lÄngsammare Àn jag drömde i mardrömmar. Slutligen visade det sig att jag ocksÄ hade gjort ett misstag i mÀtningarna... Detaljer om denna livshistoria under inlÀgget och i kommentarerna. Eftersom exemplet Àr ganska vardagligt och genomfört pÄ det sÀtt som vanligen görs i enterprise, blev det en intressant demo av livet, tycker jag: pÄverkan pÄ hastigheten av huvudsakligt Àmne i artikeln var inte mÀrkbar pÄ grund av extern logik: Moq, Autofac, EF Core och annan

Jag började mitt arbete inspirerad av denna artikel: Varför Àr refleksion lÄngsam

Som man kan se föreslÄr författaren att anvÀnda kompilerade delegater istÀllet för direkt Ätkomst till metoder frÄn reflektionsklasser som ett utmÀrkt sÀtt att kraftigt pÄskynda applikationens prestanda. Det finns förstÄs ocksÄ IL-emission, men den skulle jag vilja undvika eftersom det Àr det mest resurskrÀvande sÀttet att utföra uppgiften, vilket medför risk för fel.

Med tanke pÄ att jag alltid har haft en liknande uppfattning om reflektionshastighet hade jag inte tÀnkt ifrÄgasÀtta författarens slutsatser.

Jag stöter ofta pĂ„ naiv anvĂ€ndning av refleksion i enterprise. Man tar en typ. Man tar information om en egenskap. Metoden SetValue anropas, och alla Ă€r glada. VĂ€rdet skickas till det mĂ„lade fĂ€ltet, alla Ă€r nöjda. MĂ€nniskor Ă€r ganska intelligenta – seniorer och teamledare – skriver sina egna utvidgningar pĂ„ object baserat pĂ„ en sĂ„dan naiv implementering av 'universella' mapper frĂ„n en typ till en annan. Oftast Ă€r det sĂ„ hĂ€r: vi tar alla fĂ€lt, vi tar alla egenskaper, itererar genom dem: vid överensstĂ€mmelse av medlemsnamn utför vi SetValue. Ibland fĂ„ngar vi undantag pĂ„ missar dĂ€r vi inte hittade nĂ„gon egenskap i en av typerna, men Ă€ven hĂ€r finns det en lösning som förbĂ€ttrar prestandan. Try/catch.

Jag har sett hur mÀnniskor Äteruppfinner parsers och mappers utan att vara helt informerade om hur de cyklar som redan finns fungerar. Jag har sett hur mÀnniskor döljer sina naiva implementationer bakom strategier, bakom grÀnssnitt, bakom injektioner, som om det skulle ursÀkta den efterföljande kaoset. Jag vÀnde mig bort frÄn sÄdana implementationer. I sjÀlva verket har jag inte mÀtt den verkliga prestandaförlusten, och nÀr jag haft möjlighet har jag helt enkelt Àndrat implementeringen till en "mer optimal", om jag haft tid. DÀrför förvirrade mig de första mÀtningarna som nÀmns nedan allvarligt.

Jag tror att mÄnga av er, nÀr ni lÀser Richter eller andra ideologer, har stött pÄ det fullt rimliga pÄstÄendet att reflektion i kod Àr ett fenomen som har en extremt negativ pÄverkan pÄ applikationens prestanda.

Reflektionsanrop tvingar CLR att genomsöka assemblyer pĂ„ jakt efter den nödvĂ€ndiga, hĂ€mta deras metadata, parsa dem osv. Dessutom leder reflektion under sekvensgenomgĂ„ngar till att stora mĂ€ngder minne allokeras. NĂ€r vi anvĂ€nder minne, aktiverar CLR GC och frizarna börjar. Detta bör vara mĂ€rkbart lĂ„ngsamt, tro mig. Stora mĂ€ngder minne pĂ„ moderna produktionsservrar eller molnmaskiner rĂ€ddar inte frĂ„n höga latenser i behandlingen. Faktum Ă€r att ju mer minne, desto större Ă€r sannolikheten att du kommer att LÄGGA MÄRKE TILL hur GC fungerar. Reflektion Ă€r, i teorin, en onödig röd trasa för den.

ÄndĂ„ anvĂ€nder vi alla bĂ„de IoC-containrar och datamappers vars funktionsprincip ocksĂ„ bygger pĂ„ reflektion, men vi har vanligtvis inga frĂ„gor kring deras prestanda. Nej, inte för att beroendeinjektion och abstraktion frĂ„n modeller i en begrĂ€nsad extern kontext Ă€r sĂ„ nödvĂ€ndiga att vi mĂ„ste offra prestanda i vilket fall som helst. Det Ă€r enklare – de pĂ„verkar faktiskt inte prestandan sĂ€rskilt mycket.

Det handlar om att de mest utbredda ramverken som bygger pĂ„ reflektionsteknologi anvĂ€nder olika trick för att arbeta mer optimalt med den. Oftast handlar det om cache. Vanligtvis – det Ă€r Expressions och de delegerade som kompilerats frĂ„n expressionstrĂ€det. Denna automapper har en trĂ„d-sĂ€ker ordbok som kopplar typer till funktioner som kan konvertera en till en annan utan att kalla pĂ„ reflektion.

Hur uppnÄs detta? I grunden skiljer det sig inte frÄn logiken som plattformen sjÀlv anvÀnder för att generera JIT-kod. Vid första anropet av metoden kompileras den (och ja, denna process Àr inte snabb), vid efterföljande anrop överförs kontrollen till den redan kompilera metoden, och hÀr kommer det inte att vara nÄgra större fall i prestanda.

I vÄrt fall kan vi ocksÄ dra nytta av JIT-kompilering och sedan anvÀnda det kompilera beteendet med samma prestanda som dess AOT-motsvarigheter. I detta fall kommer uttryck att hjÀlpa oss.

Principen som diskuteras kan kort sammanfattas pÄ följande sÀtt:
Det Ă€r viktigt att cachelagra det slutliga resultatet av reflektionsarbetet i form av en delegat som innehĂ„ller den kompilera funktionen. Alla nödvĂ€ndiga objekt med typinformation bör ocksĂ„ cachas i fĂ€lt som sparas utanför objekten i din typ – workern.

Det finns logik i detta. Sunt förnuft sÀger oss att om nÄgot kan kompileras och cachas, Àr det bÀst att göra det.

För att gÄ framÄt bör det nÀmnas att cache i arbete med reflektion har sina fördelar, Àven utan att anvÀnda den föreslagna metoden för uttryckskompilering. Egentligen kommer jag hÀr att upprepa författarens punkter frÄn artikeln som jag hÀnvisade till ovan.

LÄt oss nu prata om koden. LÄt oss betrakta ett exempel som baseras pÄ en nyligen upplevd smÀrta som jag stött pÄ i en seriös produktion hos en seriös kreditorganisation. Alla entiteter Àr fiktiva, sÄ att ingen kan gissa.

Det finns en viss entitet. LÄt oss kalla den Kontakt. Det finns e-postmeddelanden med en standardiserad kropp, frÄn vilka parsern och hydratorn skapar dessa kontakter. Ett meddelande kom in, vi lÀste det, delade upp det i par av nyckel-vÀrde, skapade en kontakt och sparade den i databas.

Detta Ă€r elementĂ€rt. Anta att kontakten har egenskaperna Namn, Ålder och kontakttelefon. Dessa uppgifter överförs i meddelandet. Företaget vill ocksĂ„ att supporten snabbt ska kunna lĂ€gga till nya nycklar för att mappa egenskaperna hos entiteten mot par i meddelandets kropp. För de fall nĂ„gon gjort ett skrivfel i mallen eller om det innan lanseringen snabbt behöver starta mappningen av en ny partner, anpassa sig till det nya formatet. DĂ„ kan vi lĂ€gga till den nya mappningskorrelationen som en billig datafix. Det vill sĂ€ga, ett verkligt exempel.

Vi genomför och skapar tester. Det fungerar.

Jag kommer inte att ge koden: det blev mÄnga kÀllfiler och de Àr tillgÀngliga pÄ GitHub via lÀnken i slutet av artikeln. Du kan ladda ner dem, trötta ut dem till oigenkÀnnlighet och mÀta hur detta skulle pÄverka ditt fall. Jag ger bara koden för tvÄ mallmetoder som skiljer sig Ät mellan hydratorn som skulle vara snabb och hydratorn som skulle vara lÄngsam.

Logiken Àr följande: mallmetoden fÄr par formade av den grundlÀggande logiken i parsern. LINQ-nivÄn Àr parsern och den grundlÀggande logiken i hydratorn som gör en förfrÄgan till databasens kontext och matchar nycklar med paren frÄn parsern (det finns kod utan LINQ för dessa funktioner att jÀmföra). DÀrefter skickas paren till huvudmetoden för hydrering och vÀrdena för instÀllningen tilldelas motsvarande egenskaper hos entiteten.

"Snabb" (Prefixet Fast i benchmarken):

 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 anvĂ€nds en statisk samling med property-setters – komplierade lambdauttryck som anropar setter för entiteten. Dessa skapas med följande kod:

        static FastContactHydrator()
        {
            var type = typeof(Contact);
            foreach (var property in type.GetProperties())
            {
                _proprtySettersMap[property.Name] = GetSetterAction(property);
            }
        }

        private static Action 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>)Expression.Lambda(setterExp, paramEntity, paramValueOriginal);

            return lambda.Compile();
        }

Övergripande Ă€r det klart. Vi gĂ„r igenom egenskaperna, skapar delegater för dem som anropar setters, sparar dem. Sedan anropar vi dem nĂ€r det behövs.

"LÄngsam" (Prefixet Slow i benchmarken):

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

HÀr gÄr vi direkt igenom egenskaperna och anropar SetValue direkt.

För tydlighetens skull och som referens har jag implementerat en enkel metod som skriver vĂ€rdena av deras korrelationspar direkt i entitetsfĂ€lten. Prefixet Ă€r – Manual.

Nu tar vi BenchmarkDotNet och undersöker prestandan. Och plötsligt
 (spoiler – detta Ă€r inte rĂ€tt resultat, detaljer nedan)

Misslyckad artikel om att pÄskynda reflektion

Vad ser vi hĂ€r? Metoder som stolt bĂ€r prefixet Fast visar sig nĂ€stan under alla försök vara lĂ„ngsammare Ă€n metoder med prefixet Slow. Detta gĂ€ller bĂ„de allokering och hastighet. Å andra sidan, den vackra och eleganta implementeringen av mappning med anvĂ€ndning av LINQ-metoder dĂ€r det Ă€r möjligt, Ă€ter faktiskt upp prestandan. Skillnaden Ă€r stor. Trenden förĂ€ndras inte med olika antal genomgĂ„ngar. Skillnaden ligger bara i skalorna. Med LINQ Ă€r det 4 – 200 gĂ„nger lĂ„ngsammare, med mer skrĂ€p i ungefĂ€r samma skala.

UPPDATERAD

Jag trodde inte mina ögon, men vad viktigare Ă€r, varken mina ögon eller min kod trodde vĂ„r kollega – Dmitry Tikhonov 0x1000000. Genom att dubbelkolla min lösning upptĂ€ckte han briljant och pekade pĂ„ det fel som jag missade pĂ„ grund av en rad förĂ€ndringar i implementeringen frĂ„n början till slut. Efter att ha Ă„tgĂ€rdat den pĂ„trĂ€ffade buggen i Moq-instĂ€llningen, föll alla resultat pĂ„ plats. Vid resultatet frĂ„n retestet förĂ€ndras inte den grundlĂ€ggande trenden – LINQ pĂ„verkar fortfarande prestandan mer Ă€n reflektion. Men det Ă€r trevligt att arbetet med kompilering av uttryck görs utan anledning, och resultatet syns bĂ„de i allokering och under körningstid. Den första körningen, nĂ€r de statiska fĂ€lten initieras, Ă€r dĂ€rmed logiskt lĂ„ngsammare för den "snabba" metoden, men situationen Ă€ndras dĂ€rifrĂ„n.

HÀr Àr resultatet av retestet:

Misslyckad artikel om att pÄskynda reflektion

Slutsats: Vid anvĂ€ndning av reflektion i företagslösningar krĂ€vs inte nĂ„gra sĂ€rskilda knep – LINQ kommer att sluka prestandan mer. ÄndĂ„, i högbelastade metoder som krĂ€ver optimering, kan reflektion sparas i form av initialiserare och kompilatorer av delegationer, som sedan kommer att sĂ€kerstĂ€lla "snabb" logik. SĂ„ kan du bĂ„de bevara flexibiliteten hos reflektion och hastigheten pĂ„ applikationens arbete.

Koden med benchmarken finns hÀr. Alla intresserade kan dubbelkolla mina ord:
HabraReflectionTests

PS: koden i tester anvÀnder IoC, medan den i benchmarkar anvÀnder explicit konstruktion. Anledningen Àr att jag i den slutliga implementationen har tagit bort alla faktorer som kan pÄverka prestanda och förstöra resultatet.

PPS: Tack till anvÀndaren Dmitry Tikhonov @0x1000000 för att upptÀcka mitt misstag i instÀllningen av Moq, vilket pÄverkat de första mÀtningarna. Om nÄgon av lÀsarna har tillrÀckligt med karma, ge honom gÀrna en like. Personen stannade, lÀste noggrant, dubbelkollade och pÄpekade misstaget. Jag anser att det Àr vÀrt respekt och sympati.

PPPS: tack till den noggranna lÀsaren som klagade pÄ stilen och utformningen. Jag föresprÄkar enhetlighet och bekvÀmlighet. Diplomatin i presentationen lÀmnar en del att önska, men jag har tagit till mig kritiken. Jag ber om er förstÄelse.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster