Një artikull i dështuar mbi përshpejtimin e refleksionit

Menjëherë do ta sqaroj titullin e artikullit. Fillimisht u planifikua të jepja një këshillë të mirë dhe të besueshme mbi përshpejtimin e përdorimit të refleksionit në një shembull të thjeshtë, por realist, megjithatë, gjatë bërjes së bencmarking doli se refleksioni nuk punon aq ngadalë sa e mendova, ndërsa LINQ punon më ngadalë se çfarë më kanë ndodhur në makthet. Dhe përfundimisht u zbulua se kam bërë një gabim në matje... Detajet e kësaj historie të jetës janë poshtë dhe në komentet. Duke qenë se shembulli është mjaft i zakonshëm dhe është realizuar në mënyrën se si bëhet zakonisht në ndërmarrje, rezultoi një demostrim mjaft interesant, si mendoj unë, i jetës: ndikimi në shpejtësinë e punës së subjektit kryesor të artikullit nuk ishte i dukshëm për shkak të logjikës jashtme: Moq, Autofac, EF Core dhe përfshirjet e tjera.

Fillova punën nën ndikimin e këtij artikulli: Përse është refleksioni i ngadalshëm

Siç duket, autori sugjeron përdorimin e delegatëve të kompiluar në vend të qasjes direkte në metodat e tipeve të refleksionit si një mënyrë të shkëlqyer për të përshpejtuar dukshëm punën e aplikacionit. Ka edhe ndonjë gjë tjetër, sigurisht, emetimi IL, por do të doja ta evitonim atë, pasi është mënyra më e punës intensive për të përfunduar detyrën, e cila është e mbushur me gabime.

Duke pasur parasysh se gjithmonë kam mbajtur një mendim të ngjashëm për shpejtësinë e refleksionit, nuk kam pasur ndonjë dyshim në konkluzionet e autorit.

Nuk e takoj shpesh përdorimin naive të refleksionit në ndërmarrje. Marrim një tip. Marrim informacionin për një pronë. Thërrasim metodën SetValue dhe të gjithë gëzohemi. Vlera arrin në fushën e synuar, të gjithë janë të kënaqur. Njerëzit nuk janë të paqartë - seniorë dhe liderë ekipesh - krijojnë zgjerimet e tyre mbi objektet, duke u bazuar në një implementim kaq naive si 'mappers' universale nga një tip në tjetrin. Esenca zakonisht është: marrim të gjitha fushat, marrim të gjitha pronat, iterojmë mbi to: kur emrat e anëtarëve të tipeve përputhen, kryejmë SetValue. Herë pas here kapim përjashtime në dështimet atje ku nuk gjetëm ndonjë pronë në një nga tipet, por edhe këtu ka një zgjidhje, që rrit performancën. Try/catch.

Kam e pashë njerëzit që ri-inventonin parserët dhe mapperët, pa pasur plotësisht informacionin mbi mënyrën se si funksionojnë biçikletat e shpikura para tyre. E pashë si njerëzit fshiheshin pas strategjive, pas ndërfaqeve, pas injeksioneve, sikur kjo të justifikonte vakhanalinë që vinte më pas. Nga këto implementime, unë përthejta hundën. Në fakt, nuk kam masur ndonjë rrjedhje reale të performancës, dhe kur e kam pasur mundësinë, thjesht kam ndërruar implementimin me një më "optimal", nëse kam qenë në gjendje. Kështu që matjet e para, për të cilat flitet më poshtë, më trembën seriozisht.

Mendoj se shumica e jush, duke lexuar Richterin ose ideologë të tjerë, janë përballur me thënien e plotë të drejtë që refleksioni në kod është një fenomen që ndikon shumë negativisht në performancën e aplikacionit.

Thirrja e refleksionit e detyron CLR-nĂ« tĂ« kalojĂ« nĂ«pĂ«r ndĂ«rtimet pĂ«r tĂ« gjetur atĂ« tĂ« nevojshme, pĂ«r tĂ« tĂ«rhequr metadatet e tyre, pĂ«r t'i parsuar ato, etj. PĂ«r mĂ« tepĂ«r, refleksioni gjatĂ« kalimit tĂ« sekuencave çon nĂ« alokimin e njĂ« sasi tĂ« madhe memories. Ne harxhojmĂ« memorie, CLR hap GÇ-nĂ« dhe nisin ngadalĂ«simet. Kjo duhet tĂ« jetĂ« ndjeshĂ«m e ngadaltĂ«, besoni. Sasi tĂ« madhe memories nĂ« serverĂ«t modernĂ« tĂ« prodhimit ose makinat cloud nuk e shpĂ«tojnĂ« nga vonesat e larta nĂ« pĂ«rpunim. NĂ« fakt, sa mĂ« shumĂ« memorie, aq mĂ« e lartĂ« probabiliteti qĂ« ju tĂ« VINI RE se si funksionon GÇ-ja. Refleksioni Ă«shtĂ«, nĂ« parim, njĂ« flamur i kuq i tepĂ«rt pĂ«r tĂ«.

Megjithatë, të gjithë ne përdorim si kontenerë IoC ashtu edhe mappera të të dhënave, parimi i funksionimit të të cilëve është gjithashtu i bazuar në refleksion, por zakonisht nuk ka pyetje për performancën e tyre. Jo, jo sepse implementimi i varësive dhe abstractimi nga modelet e një konteksti të jashtëm të kufizuar janë çdo gjë që na detyron të sakrifikojmë performancën për çdo rast. Të gjitha janë më të thjeshta - ato me të vërtetë nuk ndikon shumë në performancë.

E vërteta është se framework-et më të zakonshme që i nënshtrohen teknologjisë së refleksionit përdorin të gjitha llojet e hileve për një funksionim më optimal me të. Zakonisht, ky është cache. Zakonisht - janë Expresionet dhe delegatët e kompiluar nga pemët e shprehjeve. Po ashtu, automapleri mban një fjalor konkurrues që lidh tipet me funksionet që mund të konvertojnë njëri në tjetrin pa thirrur refleksionin.

Si arrinjohet kjo? Në thelb, nuk ndryshon nga logjika që vetë platforma përdor për të gjeneruar kodin JIT. Në thirrjen e parë të metodës, kjo kompilimi (dhe, po, ky proces nuk është i shpejtë), në thirrjet e mëvonshme, kontrolli i kalon metodës së kompiluar tashmë, dhe këtu nuk do të ketë rënie të dukshme të performancës.

Në rastin tonë, gjithashtu mund të përfitojmë nga kompilimi JIT dhe më pas të përdorim sjelljen e kompiluar me të njëjtin performancë si homologët e saj AOT. Në këtë rast, do të na ndihmojnë shprehjet.

Në mënyrë të përmbledhur, mund të formulonim parimin që po flasim kështu:
Duhet të ruhet rezultati përfundimtar i punës me refleksion në formën e një delegati që përmban funksionin e kompiluar. Të gjitha objektet e nevojshme me informacion mbi tipet gjithashtu ka kuptim të ruhen në fushat e ruajtura jashtë objekteve të tipit tuaj - punëtorit.

Ka logjikë në këtë. Zgjuarsia na thotë se nëse diçka mund të kompilizohet dhe të ruhet, atëherë kjo duhet bërë.

Duke u treguar më përpara, duhet thënë se cache në punën me refleksion ka avantajet e veta, edhe nëse nuk përdoret metoda e propozuar e kompilimit të shprehjeve. Në të vërtetë, këtu do të përsëris qëndrimet e autorit të artikullit që po citoj më lart.

Tani për kodin. Le të shqyrtojmë një shembull të bazuar në dhimbjen time të fundit, me të cilën kam duhet të përballesha në një prodhim të rëndësishëm të një organizate serioze financiare. Të gjitha entitetet janë imagjinare, për të siguruar që askush të mos e kuptojë.

Ekziston një entitet. Le ta quajmë Kontakt. Ekzistojnë letra me një trup standardizues, nga të cilat parseri dhe hidratori krijojnë këto kontakte. Ka ardhur një letër, e kemi lexuar, e kemi ndarë në çifte çelësi-vlerë, krijuam kontaktin, e ruam në bazën e të dhënave.

Kjo është elementare. Le të themi se kontakti ka pronat e Emrit, Moshës dhe numrit kontaktues. Këto të dhëna dërgohen në letër. Gjithashtu, biznesi dëshiron që mbështetësit të mund të shtojnë shpejt çelësa të rinj për të mapuar pronat e entitetit me çifte në trupin e letër. Në rast se dikush ka bërë një gabim në template ose nëse duhet të aktivizohet menjëherë mapimi nga një partner i ri përpara lëshimit, duke u përshtatur me formatin e ri. Atëherë, lidhjen e re të mapimit mund ta shtojmë si një datafiks të lirë. Pra, ky është një shembull i jetës reale.

Ne realizojmë, krijojmë teste. Puna.

Nuk do tĂ« jap kodin: ka shumĂ« burime dhe ato janĂ« tĂ« disponueshme nĂ« GitHub pĂ«rmes linkut nĂ« fund tĂ« artikullit. Ju mund t’i shkarkoni, t’i torturoni deri nĂ« njohje dhe tĂ« matni se si do tĂ« ndikonte nĂ« rastin tuaj. Do tĂ« jap vetĂ«m kodin e dy metodave template, me tĂ« cilat dallohet hidratori qĂ« duhet tĂ« jetĂ« i shpejtĂ« nga hidratori qĂ« duhet tĂ« jetĂ« i ngadaltĂ«.

Logjika Ă«shtĂ« si vijon: metoda template merr çifte, tĂ« formuara nga logjika bazĂ« e parserit. Nivelin LINQ – e pĂ«rfaqĂ«son parseri dhe logjika bazĂ« e hidratorit, e cila bĂ«n njĂ« kĂ«rkesĂ« ndaj kontekstit tĂ« db dhe pĂ«rputh çelsat me çiftet nga parseri (pĂ«r kĂ«to funksione ka kod pa LINQ pĂ«r krahasim). MĂ« pas çiftet kalohen nĂ« metodĂ«n kryesore tĂ« hidrimit dhe vlerat e tyre vendosen nĂ« pronat pĂ«rkatĂ«se tĂ« entitetit.

«I shpejtë» (Prefiksi Fast në benchmarke):

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

Siç shohim, pĂ«rdoret njĂ« koleksion statik me setter-a pronash – lambda tĂ« kompiluar qĂ« thĂ«rrasin setter-in e entitetit. Krijohen me kodin nĂ« vijim:

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

Në përgjithësi, është e qartë. Qarkullojmë pronat, krijojmë delegatët për to që thërrasin setter-at, i ruajmë. Më pas, i thërrasim kur të nevojiten.

«I ngadalshëm» (Prefiksi Slow në benchmarke):

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

Këtu, menjëherë kalojmë nëpër pronat dhe thërrasim direkt SetValue.

PĂ«r ilustrim dhe si njĂ« referencĂ«, kam realizuar njĂ« metodĂ« naive qĂ« shkruan vlerat e çifteve tĂ« tyre tĂ« korrelacionit drejtpĂ«rdrejt nĂ« fushat e entitetit. Prefiksi – Manual.

Tani marrim BenchmarkDotNet dhe shqyrtojmĂ« performancĂ«n. Dhe papritmas
 (spoiler – kjo nuk Ă«shtĂ« rezultati i saktĂ«, detajet – mĂ« poshtĂ«)

Një artikull i dështuar mbi përshpejtimin e refleksionit

ÇfarĂ« shohim kĂ«tu? Metodat, qĂ« mbajnĂ« me krenari prefiksin Fast, shpesh rezultojnĂ« mĂ« tĂ« ngadalta se metodat me prefiksin Slow. Kjo Ă«shtĂ« e vĂ«rtetĂ« si pĂ«r alokimin ashtu edhe pĂ«r shpejtĂ«sinĂ« e punĂ«s. Nga ana tjetĂ«r, njĂ« implementim i bukur dhe elegant i hartimit, duke pĂ«rdorur gjithmonĂ« ku Ă«shtĂ« e mundur, metodat LINQ tĂ« dedikuara pĂ«r kĂ«tĂ«, nĂ« tĂ« vĂ«rtetĂ«, ndikon shumĂ« nĂ« performancĂ«n. Dallimi Ă«shtĂ« i rĂ«ndĂ«sishĂ«m. Tendenca nuk ndryshon me numra tĂ« ndryshĂ«m kalimesh. Dallimi ka tĂ« bĂ«jĂ« vetĂ«m me shkallĂ«t. Me LINQ nĂ« 4 – 200 herĂ« mĂ« i ngadaltĂ«, plehrat janĂ« gjithashtu mĂ« tĂ« shumta nĂ« njĂ« shkallĂ« tĂ« ngjashme.

UPDATE

Nuk i besova syve tĂ« mi, por çfarĂ« Ă«shtĂ« mĂ« e rĂ«ndĂ«sishme, as syve tĂ« mi dhe as kodit tim nuk i besoi kolegu ynĂ« — Dmitry Tikhonov 0x1000000. Duke rishikuar zgjidhjen time, ai shkĂ«lqyeshĂ«m zbuloi dhe tregoi pĂ«r gabimin qĂ« unĂ« e humba pĂ«r shkak tĂ« njĂ« numri ndryshimesh nĂ« implementimin nga fillimi nĂ« fund. Pas korrigjimit tĂ« gabimit tĂ« gjetur nĂ« konfigurimin e Moq, tĂ« gjitha rezultatet u vendosĂ«n nĂ« vendet e tyre. Sipas rezultateve tĂ« rinisjes, tendenca kryesore nuk ndryshon – LINQ ndikon nĂ« performancĂ« pĂ«r shkak se Ă«shtĂ« mĂ« i fortĂ« se refleksioni. MegjithatĂ«, Ă«shtĂ« kĂ«naqĂ«si qĂ« puna me kompilimin e shprehjeve bĂ«het pa kot dhe rezultati vihet re si nĂ« alokim ashtu edhe nĂ« kohĂ«n e ekzekutimit. LĂ«shimi i parĂ«, kur inicializohen fushat statike, e ka natyrshĂ«m mĂ« tĂ« ngadalshme metodĂ«n "rapid", por mĂ« vonĂ« situata ndryshon.

Këtu është rezultati i rinisjes:

Një artikull i dështuar mbi përshpejtimin e refleksionit

PĂ«rfundimi: Kur pĂ«rdoret nĂ« ndĂ«rmarrje, nuk kĂ«rkohet e vecanta e refleksionit — LINQ do tĂ« konsumojĂ« performancĂ«n mĂ« shumĂ«. MegjithatĂ«, nĂ« metodat me ngarkesĂ« tĂ« lartĂ« qĂ« kĂ«rkojnĂ« optimizim, mund tĂ« ruajmĂ« refleksionin nĂ« formĂ«n e inicializatorĂ«ve dhe kompjutorĂ«ve tĂ« delegatĂ«ve, qĂ« do tĂ« ofrojnĂ« mĂ« vonĂ« logjikĂ«n "e shpejtĂ«". NĂ« kĂ«tĂ« mĂ«nyrĂ«, mund tĂ« ruani fleksibilitetin e refleksionit dhe shpejtĂ«sinĂ« e punĂ«s sĂ« aplikacionit.

Kodi me benchmark është i disponueshëm këtu. Të gjithë të interesuarit mund të rishikojnë fjalët e mia:
HabraReflectionTests

PS: kodi nĂ« teste pĂ«rdor IoC, ndĂ«rsa nĂ« benchmark-e – njĂ« konstrukcion tĂ« qartĂ«. E gjitha qĂ«ndron nĂ« faktin se nĂ« realizimin pĂ«rfundimtar kam eliminuar tĂ« gjitha faktorĂ«t qĂ« mund tĂ« ndikojnĂ« nĂ« performancĂ«n dhe tĂ« zhurmojnĂ« rezultatet.

PPS: Faleminderit përdoruesit Dmitry Tikhonov @0x1000000 për zbulimin e gabimit tim në konfigurimin e Moq, i cili ndikoi në matjet e para. Nëse ndonjë prej lexuesve ka mjaft karma, lutem të pëlqeni atë. Një njeri u ndal, një njeri lexoi me kujdes, një njeri e rishikoi dhe tregoi për gabimin. Unë mendoj se kjo është për t'u respektuar dhe simpatizuar.

PPPS: faleminderit atij lexuesi të ndjeshëm, i cili u merret me stilin dhe formatimin. Unë jam për njësi dhe rehati. Diplomacia e paraqitjes lë për të dëshiruar, por unë e kam marrë parasysh kritikën. Ju lutem, vijoni.

Burimi: habr.com

Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster