Artikull i pasuksesshëm për përshpejtimin e reflektimit

Unë do të shpjegoj menjëherë titullin e artikullit. Plani fillestar ishte të jepte këshilla të mira dhe të besueshme se si të përshpejtonim përdorimin e reflektimit duke përdorur një shembull të thjeshtë por realist, por gjatë krahasimit doli që reflektimi nuk është aq i ngadaltë sa mendoja, LINQ është më i ngadalshëm sesa në makthet e mia. Por në fund doli që edhe unë kam gabuar në matjet... Detajet e kësaj historie jete janë nën prerje dhe në komente. Meqenëse shembulli është mjaft i zakonshëm dhe zbatohet në parim siç bëhet zakonisht në një ndërmarrje, doli të ishte një demonstrim mjaft interesant, siç më duket mua, i jetës: ndikimi në shpejtësinë e temës kryesore të artikullit ishte nuk vërehet për shkak të logjikës së jashtme: Moq, Autofac, EF Core dhe "rripa" të tjerë.

Fillova të punoj nën përshtypjen e këtij artikulli: Pse Reflektimi është i ngadalshëm

Siç mund ta shihni, autori sugjeron përdorimin e delegatëve të përpiluar në vend të thirrjes së drejtpërdrejtë të metodave të llojit të reflektimit si një mënyrë e shkëlqyeshme për të shpejtuar shumë aplikacionin. Sigurisht, ka emetim IL, por unë do të doja ta shmangja atë, pasi kjo është mënyra më intensive për të kryer detyrën, e cila është e mbushur me gabime.

Duke pasur parasysh që gjithmonë kam pasur një mendim të ngjashëm për shpejtësinë e reflektimit, nuk kam pasur në plan të vë në dyshim përfundimet e autorit.

Shpesh ndeshem me përdorim naiv të reflektimit në ndërmarrje. Lloji është marrë. Informacioni për pronën është marrë. Thirret metoda SetValue dhe të gjithë gëzohen. Vlera ka mbërritur në fushën e synuar, të gjithë janë të lumtur. Njerëz shumë të zgjuar - të moshuarit dhe drejtuesit e ekipit - shkruajnë shtesat e tyre për të kundërshtuar, duke u bazuar në një zbatim kaq naiv, hartuesit "universalë" të një lloji në tjetrin. Thelbi zakonisht është ky: ne marrim të gjitha fushat, marrim të gjitha vetitë, përsërisim mbi to: nëse emrat e anëtarëve të tipit përputhen, ne ekzekutojmë SetValue. Herë pas here ne kapim përjashtime për shkak të gabimeve ku nuk kemi gjetur ndonjë pronë në një nga llojet, por edhe këtu ka një rrugëdalje që përmirëson performancën. Provoni/kapni.

Kam parë njerëz që rishpikin analizues dhe hartues pa qenë plotësisht të armatosur me informacione se si funksionojnë makinat që dolën përpara tyre. Kam parë njerëz që fshehin zbatimet e tyre naive pas strategjive, pas ndërfaqeve, pas injeksioneve, sikur kjo do të justifikonte bacchanalia pasuese. Ngrita hundën në realizime të tilla. Në fakt, nuk e mata rrjedhjen reale të performancës dhe, nëse ishte e mundur, thjesht e ndryshova zbatimin në një më "optimal" nëse mund ta kapja. Prandaj, matjet e para të diskutuara më poshtë më hutuan seriozisht.

Mendoj se shumë prej jush, duke lexuar Richter apo ideologë të tjerë, kanë hasur në një deklaratë krejtësisht të drejtë se reflektimi në kod është një fenomen që ka një ndikim jashtëzakonisht negativ në performancën e aplikacionit.

Reflektimi i thirrjeve e detyron CLR të kalojë nëpër asamble për të gjetur atë që i nevojitet, të tërheqë meta të dhënat e tyre, t'i analizojë ato, etj. Përveç kësaj, reflektimi gjatë kalimit të sekuencave çon në shpërndarjen e një sasie të madhe memorie. Ne po përdorim memorien, CLR zbulon GC-në dhe frizat fillojnë. Duhet të jetë dukshëm i ngadalshëm, më besoni. Sasitë e mëdha të memories në serverët e prodhimit modern ose makinat cloud nuk parandalojnë vonesat e larta të përpunimit. Në fakt, sa më shumë memorie, aq më shumë ka të ngjarë të VËREJNI se si funksionon GC. Reflektimi është, në teori, një leckë e kuqe shtesë për të.

Sidoqoftë, ne të gjithë përdorim kontejnerët IoC dhe hartuesit e datave, parimi i funksionimit të të cilave bazohet gjithashtu në reflektim, por zakonisht nuk ka pyetje në lidhje me performancën e tyre. Jo, jo sepse futja e varësive dhe abstragimi nga modelet e jashtme të kontekstit të kufizuar janë aq të nevojshme sa duhet të sakrifikojmë performancën në çdo rast. Gjithçka është më e thjeshtë - me të vërtetë nuk ndikon shumë në performancën.

Fakti është se kornizat më të zakonshme që bazohen në teknologjinë e reflektimit përdorin të gjitha llojet e trukeve për të punuar me të në mënyrë më optimale. Zakonisht kjo është një cache. Zakonisht këto janë Shprehje dhe delegatë të përpiluar nga pema e shprehjes. I njëjti hartues automatik mban një fjalor konkurrues që përputh tipat me funksionet që mund të konvertojnë njëra në tjetrën pa thirrur reflektim.

Si arrihet kjo? Në thelb, kjo nuk ndryshon nga logjika që vetë platforma përdor për të gjeneruar kodin JIT. Kur një metodë thirret për herë të parë, ajo përpilohet (dhe, po, ky proces nuk është i shpejtë); në thirrjet pasuese, kontrolli transferohet në metodën tashmë të përpiluar dhe nuk do të ketë ulje të konsiderueshme të performancës.

Në rastin tonë, mund të përdorni gjithashtu përpilimin JIT dhe më pas të përdorni sjelljen e përpiluar me të njëjtën performancë si homologët e tij AOT. Shprehjet do të na vijnë në ndihmë në këtë rast.

Parimi në fjalë mund të formulohet shkurtimisht si më poshtë:
Ju duhet të ruani rezultatin përfundimtar të reflektimit si një delegat që përmban funksionin e përpiluar. Ka gjithashtu kuptim që të gjitha objektet e nevojshme me informacionin e tipit të ruhen në memorie në fushat e llojit tuaj, punëtori, që ruhen jashtë objekteve.

Ka logjikë në këtë. Mendja e shëndoshë na thotë se nëse diçka mund të kompilohet dhe ruhet në memorie, atëherë duhet bërë.

Duke parë përpara, duhet thënë se cache në punën me reflektim ka avantazhet e saj, edhe nëse nuk përdorni metodën e propozuar të përpilimit të shprehjeve. Në fakt, këtu po përsëris thjesht tezat e autorit të artikullit të cilit i referohem më lart.

Tani në lidhje me kodin. Le të shohim një shembull që bazohet në dhimbjen time të fundit që më është dashur të përballem në një prodhim serioz të një institucioni serioz krediti. Të gjitha entitetet janë fiktive në mënyrë që askush të mos e marrë me mend.

Ka një thelb. Le të ketë Kontakt. Ka shkronja me trup të standardizuar, nga të cilat analizuesi dhe hidratori krijojnë të njëjtat kontakte. Arriti një letër, e lexuam, e analizuam në çifte me vlerë kyçe, krijuam një kontakt dhe e ruajmë në bazën e të dhënave.

Është elementare. Le të themi se një kontakt ka vetitë Emri i plotë, Mosha dhe Telefoni i kontaktit. Këto të dhëna transmetohen në letër. Biznesi gjithashtu kërkon mbështetje që të jetë në gjendje të shtojë shpejt çelësat e rinj për hartëzimin e vetive të entitetit në çifte në trupin e letrës. Në rast se dikush ka bërë një gabim shtypi në shabllon ose nëse para publikimit është e nevojshme të nisni urgjentisht hartëzimin nga një partner i ri, duke iu përshtatur formatit të ri. Më pas mund të shtojmë një korrelacion të ri të hartës si një fiks i lirë të dhënash. Kjo është një shembull i jetës.

Ne zbatojmë, krijojmë teste. Punimet.

Unë nuk do të jap kodin: ka shumë burime dhe ato janë të disponueshme në GitHub përmes lidhjes në fund të artikullit. Mund t'i ngarkoni, t'i torturoni përtej njohjes dhe t'i matni, siç do të ndikonte në rastin tuaj. Do të jap vetëm kodin e dy metodave shabllone që dallojnë hidratuesin, i cili supozohej të ishte i shpejtë, nga hidratori që supozohej të ishte i ngadalshëm.

Logjika është si më poshtë: metoda shabllon merr çifte të krijuara nga logjika bazë e analizës. Shtresa LINQ është analizuesi dhe logjika bazë e hidratorit, i cili bën një kërkesë në kontekstin e bazës së të dhënave dhe krahason çelësat me çiftet nga parser (për këto funksione ka kod pa LINQ për krahasim). Më pas, çiftet kalohen në metodën kryesore të hidratimit dhe vlerat e çifteve vendosen në vetitë përkatëse të entitetit.

"Fast" (Prefiksi Fast në standardet):

 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ç mund ta shohim, përdoret një koleksion statik me vetitë e setterit - lambda të përpiluara që thërrasin entitetin e vendosësit. Krijuar nga kodi i mëposhtëm:

        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ë. Ne përshkojmë vetitë, krijojmë delegatë për ta që thërrasin vendosësit dhe i ruajmë. Pastaj telefonojmë kur është e nevojshme.

"Slow" (Prefiksi Slow në standardet):

        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 menjëherë anashkalojmë vetitë dhe thërrasim drejtpërdrejt SetValue.

Për qartësi dhe si referencë, zbatova një metodë naive që shkruan vlerat e çifteve të tyre të korrelacionit direkt në fushat e entitetit. Prefiksi – Manual.

Tani le të marrim BenchmarkDotNet dhe të shqyrtojmë performancën. Dhe befas... (spoiler - ky nuk është rezultati i saktë, detajet janë më poshtë)

Artikull i pasuksesshëm për përshpejtimin e reflektimit

Çfarë shohim këtu? Metodat që mbajnë triumfalisht parashtesën Fast rezultojnë të jenë më të ngadalta në pothuajse të gjitha kalimet sesa metodat me parashtesën Slow. Kjo është e vërtetë si për shpërndarjen ashtu edhe për shpejtësinë e punës. Nga ana tjetër, një zbatim i bukur dhe elegant i hartës duke përdorur metoda LINQ të destinuara për këtë kudo që të jetë e mundur, përkundrazi, redukton shumë produktivitetin. Dallimi është i rendit. Trendi nuk ndryshon me numra të ndryshëm kalimesh. Dallimi i vetëm është në shkallë. Me LINQ është 4 - 200 herë më i ngadalshëm, ka më shumë mbeturina në përafërsisht të njëjtën shkallë.

UPDATED

Unë nuk u besova syve të mi, por më e rëndësishmja, kolegu ynë nuk u besoi as syve, as kodit tim - Dmitry Tikhonov 0x1000000. Pasi kontrolloi dy herë zgjidhjen time, ai zbuloi shkëlqyeshëm dhe vuri në dukje një gabim që e humba për shkak të një sërë ndryshimesh në zbatim, nga fillimi në përfundimtar. Pas rregullimit të defektit të gjetur në konfigurimin e Moq, të gjitha rezultatet ranë në vend. Sipas rezultateve të ritestimit, tendenca kryesore nuk ndryshon - LINQ akoma ndikon në performancën më shumë sesa reflektimin. Sidoqoftë, është mirë që puna me përpilimin e Expression nuk është bërë më kot, dhe rezultati është i dukshëm si në kohën e shpërndarjes ashtu edhe në ekzekutimin. Nisja e parë, kur fushat statike inicializohen, është natyrisht më e ngadaltë për metodën "e shpejtë", por më pas situata ndryshon.

Këtu është rezultati i ritestimit:

Artikull i pasuksesshëm për përshpejtimin e reflektimit

Përfundim: kur përdorni reflektimin në një ndërmarrje, nuk ka nevojë të veçantë për t'u përdorur në truket - LINQ do të hajë më shumë produktivitetin. Sidoqoftë, në metodat me ngarkesë të lartë që kërkojnë optimizim, mund të ruani reflektimin në formën e iniciatorëve dhe të delegoni kompajlerët, të cilët më pas do të ofrojnë logjikë "të shpejtë". Në këtë mënyrë ju mund të ruani si fleksibilitetin e reflektimit ashtu edhe shpejtësinë e aplikimit.

Kodi i standardit është i disponueshëm këtu. Çdokush mund të kontrollojë dy herë fjalët e mia:
Testet Habra Reflection

PS: kodi në teste përdor IoC, dhe në standardet përdor një konstrukt eksplicit. Fakti është se në zbatimin përfundimtar unë ndërpreva të gjithë faktorët që mund të ndikojnë në performancën dhe ta bëjnë rezultatin të zhurmshëm.

PPS: Faleminderit përdoruesit Dmitry Tikhonov @0x1000000 për zbulimin e gabimit tim në vendosjen e Moq, i cili ndikoi në matjet e para. Nëse ndonjë nga lexuesit ka karma të mjaftueshme, ju lutemi pëlqeni. Burri ndaloi, burri lexoi, burri kontrolloi dy herë dhe tregoi gabimin. Unë mendoj se kjo është e denjë për respekt dhe simpati.

PPPS: faleminderit lexuesit të përpiktë që arriti në fund të stilit dhe dizajnit. Unë jam për uniformitetin dhe komoditetin. Diplomacia e prezantimit lë për të dëshiruar, por kritikën e kam marrë parasysh. Kërkoj predhën.

Burimi: www.habr.com

Shto një koment