Një artikull i dështuar rreth përshpejtimit të refleksionit

Menjëherë do të shpjegoj titullin e artikullit. Fillimisht ishte planifikuar të jepja një këshillë të mirë dhe të besueshme për përshpejtimin e përdorimit të reflektimit në një shembull të thjeshtë, por realist, megjithatë, gjatë benchmark-ut doli se refleksioni nuk funksionon aq ngadalë sa e mendoja, ndërsa LINQ punon më ngadalë se çfarë kisha ëndërruar në makthe. Dhe përfundimisht, doli se kisha bërë një gabim në matje... Detajet e kësaj historie jetësore janë më poshtë dhe në komentet. Duke qenë se shembulli është mjaft i zakonshëm dhe i realizuar në parim ashtu siç bëhet zakonisht në ndërmarrje, rezultoi një demonstrim mjaft interesant, siç më duket, i jetës: ndikimet në shpejtësinë e punës së objektit themelor të artikullit nuk ishin të dukshme për shkak të logjikës së jashtme: Moq, Autofac, EF Core dhe të tjera 'lidhëse'.

E nisa punën nën ndikimin e këtij artikulli: Pse është refleksioni i ngadalshëm

Siç duket, autori sugjeron të përdoren delegatët e kompiluara në vend të qasjes direkte në metodat e tipeve të reflektimit si një mënyrë e shkëlqyer për ta përshpejtuar ndjeshëm punën e aplikacionit. Natyrisht, atje ka gjithashtu emision IL, por do të doja ta evitja atë, sepse është mënyra më e punësshme për ta realizuar detyrën, e cila është e mbushur me gabime.

Duke marrë parasysh se gjithmonë kam mbajtur një opinion të ngjashëm për shpejtësinë e reflektimit, nuk do të doja ta vë në dyshim konkluzionin e autorit.

Nuk e takoj rrallë përdorimin naive të reflektimit në ndërmarrje. Merret një tip. Merret informacioni rreth një prone. Thirret metoda SetValue, dhe të gjithë gëzohen. Vlera arriti në fushën e caktuar, të gjithë janë të kënaqur. Njerëzit janë mjaft të zgjuar - seniorë dhe liderë ekipesh - shkruajnë zgjerimet e tyre në objekt, duke u bazuar në një implementim të tillë naive 'universale' për të mapuar një tip në një tjetër. Zgjidhja është zakonisht: marrim të gjitha fushat, marrim të gjitha pronat, eiterojmë ato: në rast të përputhjes së emrave të anëtarëve të tipit realizojmë SetValue. Periodikisht kapim përjashtime për humbjet atje, ku nuk gjetëm ndonjë pronë në një nga tipat, por edhe këtu ka një dalje që përmirëson performancën. Try/catch.

Kam kam shoh një herë se si njerëzit risjellin parserat dhe mapperat, pa qenë plotësisht të armatosur me informacionin se si funksionojnë biçikletat e shpikura para tyre. Kam parë si njerëzit fshehin implementimet e tyre naive pas strategjive, pas ndërfaqeve, pas injeksioneve, sikur kjo do të justifikonte kaosin e mëvonshëm. Mënyrat e tilla të implementimit më vinin në mëri. Në fakt, nuk kam matur asnjë herë humbje reale performante, dhe kur kam pasur mundësi, thjesht e kam ndërruar implementimin me një më 'optimal', kur duar më krijonin mundësi. Prandaj, matjet e para, për të cilat flitet më poshtë, më befasuan seriozisht.

Mendoj se shumë prej jush, duke lexuar Richterin ose ideologët e tjerë, janë ndeshur me pohimin mjaft të drejtë se refleksioni në kod është një fenomen që ndikon shumë negativisht në performancën e aplikacionit.

Thirrja e refleksionit e detyron CLR tĂ« kalojĂ« pĂ«rmes skedarĂ«ve duke kĂ«rkuar atĂ« qĂ« i nevojitet, tĂ« tĂ«rheqĂ« metadata e tyre, t'i analizojĂ« etj. PĂ«r mĂ« tepĂ«r, refleksioni gjatĂ« kalimit tĂ« sekvencave çon nĂ« alokimin e njĂ« sasi tĂ« madhe memorie. Duke harxhuar memorie, CLR e aktivizon GC dhe nisin frikĂ«sitĂ«. Kjo duhet tĂ« jetĂ« dukshĂ«m e ngadalshme, besoni. Sasi tĂ« mĂ«dha memorie tĂ« serverave modernĂ« nĂ« prodhim ose makinave cloud nuk shpĂ«tojnĂ« nga vonesat e larta nĂ« pĂ«rpunim. NĂ« fakt, aq mĂ« shumĂ« memorie, aq mĂ« e madhe mundĂ«sia qĂ« do TË VINI RE si funksionon GC. Refleksioni – nĂ« parim, Ă«shtĂ« njĂ« flamur i tepĂ«rt pĂ«r tĂ«.

MegjithatĂ«, tĂ« gjithĂ« ne pĂ«rdorim edhe kontejnerĂ«t IoC dhe mapperat e tĂ« dhĂ«nave, parimi i punĂ«s sĂ« tĂ« cilĂ«ve gjithashtu bazohet nĂ« refleksion, por zakonisht nuk ka dyshime pĂ«r performancĂ«n e tyre. Jo, jo sepse injektimi i varĂ«sive dhe abstraksioni nga modelet e kontekstit tĂ« kufizuar jashtĂ« janĂ« gjĂ«ra kaq tĂ« nevojshme, saqĂ« ne kemi treguar sakrificĂ« nĂ« çdo rast pĂ«r performancĂ«n. Gjithçka Ă«shtĂ« mĂ« e thjeshtĂ« – kjo vĂ«rtet nuk ndikon shumĂ« nĂ« performancĂ«.

Çështja Ă«shtĂ« se frameworkĂ«t mĂ« tĂ« zakonshĂ«m, tĂ« cilĂ«t janĂ« tĂ« bazuar nĂ« teknologjinĂ« e refleksionit, pĂ«rdorin çdo lloj truku pĂ«r tĂ« punuar mĂ« optimalisht me tĂ«. Zakonisht, kjo Ă«shtĂ« cache. Zakonisht – kjo Ă«shtĂ« Expressions dhe delegatĂ«t e pĂ«rkthyer nga pemĂ«t e shprehjeve. I njĂ«jti automapper mbajti njĂ« fjalor konkurrues, qĂ« i dukej tipeve me funksionet qĂ« mund tĂ« konvertojnĂ« njĂ«ri-tjetrin tashmĂ« pa thirrjen e refleksionit.

Si e arrihet 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, ajo kompilohet (dhe, po, ky proces nuk është i shpejtë), ndërsa në thirrjet e mëpasshme, menaxhimi kalon te metoda e kompiliuar, dhe këtu nuk do të ketë humbje të veçanta të performancës.

Në rastin tonë, gjithashtu mund të shfrytëzojmë kompilimin JIT dhe më pas të përdorim sjelljen e kompiluar me të njëjtën performancë si homologët e saj AOT. Shprehjet do të na ndihmojnë në këtë rast.

Në mënyrë të shkurtër, parimi për të cilin flitet mund të formulohet si më poshtë:
Kjo Ă«shtĂ« e rekomandueshme qĂ« tĂ« keshojmĂ« rezultatin pĂ«rfundimtar tĂ« veprimit tĂ« refleksionit nĂ« formĂ«n e njĂ« delegati qĂ« pĂ«rmban funksionin e kompiluar. TĂ« gjithĂ« objektet e nevojshme me informacionin pĂ«r llojet gjithashtu ka kuptim tĂ« keshohen nĂ« fushat e ruajtura jashtĂ« objekteve tĂ« tipit tuaj – punĂ«torit.

Ka logjikë në këtë. Shëndeti i arsyes na thotë se nëse diçka mund të kompozohet dhe keshohet, duhet ta bëjmë atë.

Duke ia mbivendosur, duhet të themi se keshi në punën me refleksionin ka avantazhet e tij, edhe nëse nuk përdorim metodën e ofruar të kompilimit të shprehjeve. Fjalë për fjalë, këtu do të përsëris thënie të autorit të artikullit që referohem më sipër.

Tani për kodin. Le të shqyrtojmë një shembull, i bazuar në dhimbjen time të fundit, me të cilën kam përballuar në një prodhim serioz të një organizate krediti të rëndësishme. Të gjitha entitetet janë imagjinare, për të ndihmuar që askush të mos e kuptojë.

Ekziston një entitet. Le të jetë Kontakton. Ka letra me një trup të standardizuar, nga të cilat parseri dhe hidratori krijojnë këto kontakte. Më erdhi një letër, ne e lexuam atë, e analizuam në çiftet çelës-vlerë, krijuam kontaktin, e ruajtëm në databazë.

Kjo është elementare. Le të supozojmë se kontakti ka pronat Emri, Mosha dhe numri i kontaktit. Këto të dhëna transferohen në letër. Po ashtu, biznesi dëshiron që mbështetësit të mund të shtonin shpejt çelësa të rinj për mapimin e pronave të entitetit në çiftet e trupit të letrës. Në rast se dikush gaboi në modelin ose nëse është e nevojshme të nisim urgjentisht mapimin nga një partner i ri, duke u përshtatur me formatin e ri. Atëherë, ne do të mund ta shtojmë një korrelacion të ri të mapimit si një riparim të lirë të të dhënave. Pra, ky është një shembull jetësor.

E realizojmë, krijojmë teste. Funksionon.

Nuk do të jap kodin: burimet u bënë shumë, dhe ato janë të disponueshme në GitHub në linkun në fund të artikullit. Mund t'i shkarkoni, t'i testoni deri në pamjen e papërshkrueshme dhe të maturoni se si do t'u shkonte në rastin tuaj. Do të tregoj vetëm kodin e dy metodave model, me të cilat ndryshojnë hidratori që duhet të jetë i shpejtë nga hidratori që duhet të jetë i ngadalshëm.

Logjika Ă«shtĂ« kĂ«shtu: metoda model merr çiftet e formuara nga logjika baze e parserit. Niveli LINQ – Ă«shtĂ« parseri dhe logjika bazĂ« e hidratorit, e cila bĂ«n njĂ« kĂ«rkesĂ« nĂ« kontekstin e databazĂ«s dhe pĂ«rputh çiftet me çelĂ«sat nga parseri (pĂ«r kĂ«to funksione ka kod pa LINQ pĂ«r krahasim). Pastaj çiftet dĂ«rgohen nĂ« metodĂ«n kryesore tĂ« hidrimit dhe vlerat e parametrave vendosen nĂ« pronat pĂ«rkatĂ«se tĂ« entitetit.

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

 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 vendosĂ«s tĂ« pronave – me lambda tĂ« kompiluar, duke thirrur vendosĂ«sin e entitetit. KĂ«to 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<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();
        }

Në përgjithësi është e qartë. Kalojmë nëpër pronat, krijojmë delegatët për to që thërrasin vendosësit, i ruajmë. Më pas i thërrasim kur është e nevojshme.

«I ngadaltë» (Prefiksi Slow në benchmarkë):

        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 ne kalojmë menjëherë nëpër pronat dhe thërrasim drejtpërsëdrejti SetValue.

PĂ«r transparencĂ« dhe si model, realizova njĂ« metodĂ« naive, e cila shkruan vlerat e çiftĂ«ve tĂ« korrelacionit drejtpĂ«rsĂ«drejti nĂ« fushat e entitetit. Prefiksi – Manual.

Tani marrim BenchmarkDotNet dhe shqyrtojmë performancën. Dhe papritmas... (spoiler - nuk është rezultati i duhur, detajet - më poshtë)

Një artikull i dështuar rreth përshpejtimit të refleksionit

ÇfarĂ« shohim kĂ«tu? Metodat qĂ« mbajnĂ« pĂ«rcaktimin Fast, nĂ« shumicĂ«n e rasteve dalin mĂ« tĂ« ngadalta se metodat me pĂ«rcaktimin 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 mapimit qĂ« pĂ«rdor gjithandej metodat e LINQ shkatĂ«rron ndjeshĂ«m performancĂ«n. Diferenca Ă«shtĂ« nĂ« rend. Tendenca nuk ndryshon me numra tĂ« ndryshĂ«m kalimesh. Diferenca Ă«shtĂ« vetĂ«m nĂ« shkallĂ«. Me LINQ Ă«shtĂ« 4 – 200 herĂ« mĂ« e ngadalshme, dhe mbetjet e mbeturinave rriten nĂ« pĂ«rmasa tĂ« ngjashme.

PËRMIRESUAR

Nuk i besoja syve tĂ« mi, por mĂ« e rĂ«ndĂ«sishmja, as syve tĂ« mi, as kodit tim nuk i besoi kolegu ynĂ« — Dmitry Tikhonov 0x1000000. Duke kontrolluar zgjidhjen time, ai fantastikisht zbuloi dhe e theksoi gabimin qĂ« unĂ« e kisha humbur pĂ«r shkak tĂ« disa ndryshimeve nga implementimi nga fillimi nĂ« fund. Pas rregullimit tĂ« gabimit tĂ« gjetur nĂ« konfigurimin e Moq, tĂ« gjitha rezultatet u vendosen nĂ« vendin e tyre. Pas rezultatĂ«ve tĂ« ri-testimit, tendenca kryesore nuk ndryshon — LINQ ndikon mĂ« shumĂ« nĂ« performancĂ« se sa refleksioni. MegjithatĂ«, Ă«shtĂ« e kĂ«ndshme qĂ« puna me kompilimin e Expression-ve nuk Ă«shtĂ« bĂ«rĂ« kot, dhe rezultati Ă«shtĂ« i dukshĂ«m si nĂ« alokim, ashtu edhe nĂ« kohĂ«n e ekzekutimit. Goditja e parĂ«, kur inicializohen fushat statike, Ă«shtĂ« mĂ« e ngadalshme pĂ«r metodĂ«n “tĂ« shpejtĂ«â€, por mĂ« pas situata ndryshon.

Ja rezultati i ri-testimit:

Një artikull i dështuar rreth përshpejtimit të refleksionit

PĂ«rfundimi: kur pĂ«rdorim refleksionin nĂ« ndĂ«rmarrje, nuk kĂ«rkohet shumĂ« hile — LINQ do tĂ« konsumojĂ« mĂ« shumĂ« performancĂ«. MegjithatĂ«, nĂ« metodat me ngarkesĂ« tĂ« lartĂ« qĂ« kĂ«rkojnĂ« optimizim, mund tĂ« ruhet refleksioni si inicializatorĂ« dhe kompilatorĂ« delegatĂ«sh, tĂ« cilĂ«t mĂ« pas do tĂ« ofrojnĂ« logjikĂ«n “tĂ« shpejtĂ«â€. KĂ«shtu ju 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ë që duan mund të verifikojnë fjalët e mia:
HabraReflectionTests

PS: kodi në teste përdor IoC, ndërsa në benchmark - strukturën e dukshme. Problemi është se në implementimin përfundimtar kam pastruar të gjitha faktorët që mund të ndikojnë në performancë dhe të zhurmojnë rezultatin.

PPS: Faleminderit përdoruesit Dmitry Tikhonov @0x1000000 për zbulimin e gabimit tim në konfigurimin e Moq, që ndikoi në matjet e para. Nëse dikush nga lexuesit ka mjaft karma, ju lutem i jepni një like. Njeriu u ndal, njeri u angazhua, njeri kontrolloi dhe theksoi gabimin. Unë besoj se kjo është e merituar dhe e respektuar.

PPPS: faleminderit atij lexuesi të përkushtuar, i cili shqyrtoi stilin dhe formën. Unë jam për njësi dhe komoditet. Diplomacia e paraqitjes lë shumë për të dëshiruar, por unë e kam marrë parasysh kritikat. Ju lutem, rrini gati.

Burimi: habr.com

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